From 161ac4c2f78723590b2c4fc75d176cbee91399c3 Mon Sep 17 00:00:00 2001 From: rdb Date: Tue, 15 Feb 2022 17:49:46 +0100 Subject: [PATCH] pstats: Another major update for PStats server UI, including: - New powerful scrolling Timeline view for seeing all time events across all threads - Redo flame graph to use stack-based nesting rather than the standard collector nesting - Rewrite flame graph drawing to not use labels - Status bar appears in main window showing top-level level collectors; double-clicking them brings up their chart and right-clicking them shows their children - Context menus are added when right-clicking labels and charts - Tooltips now appear when mouse hovers over collector area in a chart - Strip chart windows now automatically determine the appropriate scale better - Graph menus redone to allow opening flame chart anywhere as well as strip chart - Instead of just ms everywhere, also use s / us / ns where appropriate - Don't disable smoothing right away on mouse down on strip chart, only after dragging - Windows: The MDI child windows are quite ugly and overlap with the status bar, so instead they are now top-level windows, but some code is added to make them spawn inside and move with the parent window, and minimize to its corner. I can back this out if people prefer the old behavior despite the ugly decoration - Windows: Label text shows ellipsis when cut off - Windows: Graph windows no longer have icons - Windows: Graph windows no longer spawn perfectly on top of each other, rather cascading - GTK: Render at high resolution when GDK_SCALE is not 1 - GTK: Graph windows are forced to be floating in tiling WMs - GTK: Flame chart window no longer has useless dividing bar - GTK: Use more efficient cairo surface types --- pandatool/src/gtk-stats/CMakeLists.txt | 1 + pandatool/src/gtk-stats/gtkStatsChartMenu.cxx | 148 ++-- pandatool/src/gtk-stats/gtkStatsChartMenu.h | 1 - .../src/gtk-stats/gtkStatsFlameGraph.cxx | 321 ++++--- pandatool/src/gtk-stats/gtkStatsFlameGraph.h | 23 +- pandatool/src/gtk-stats/gtkStatsGraph.cxx | 288 +++++-- pandatool/src/gtk-stats/gtkStatsGraph.h | 59 +- pandatool/src/gtk-stats/gtkStatsLabel.cxx | 18 +- pandatool/src/gtk-stats/gtkStatsMonitor.I | 10 +- pandatool/src/gtk-stats/gtkStatsMonitor.cxx | 234 ++++- pandatool/src/gtk-stats/gtkStatsMonitor.h | 29 +- pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx | 173 +++- pandatool/src/gtk-stats/gtkStatsPianoRoll.h | 14 +- .../src/gtk-stats/gtkStatsStripChart.cxx | 210 +++-- pandatool/src/gtk-stats/gtkStatsStripChart.h | 13 +- pandatool/src/gtk-stats/gtkStatsTimeline.cxx | 812 ++++++++++++++++++ pandatool/src/gtk-stats/gtkStatsTimeline.h | 91 ++ .../src/gtk-stats/gtkstats_composite1.cxx | 1 + pandatool/src/pstatserver/CMakeLists.txt | 14 +- .../pstatserver/p3pstatserver_composite1.cxx | 1 + pandatool/src/pstatserver/pStatFlameGraph.I | 33 +- pandatool/src/pstatserver/pStatFlameGraph.cxx | 432 +++++++--- pandatool/src/pstatserver/pStatFlameGraph.h | 76 +- pandatool/src/pstatserver/pStatGraph.cxx | 38 +- pandatool/src/pstatserver/pStatGraph.h | 1 + pandatool/src/pstatserver/pStatPianoRoll.I | 8 + pandatool/src/pstatserver/pStatPianoRoll.cxx | 14 + pandatool/src/pstatserver/pStatPianoRoll.h | 4 + pandatool/src/pstatserver/pStatStripChart.I | 8 + pandatool/src/pstatserver/pStatStripChart.cxx | 49 +- pandatool/src/pstatserver/pStatStripChart.h | 1 + pandatool/src/pstatserver/pStatThreadData.cxx | 8 + pandatool/src/pstatserver/pStatTimeline.I | 139 +++ pandatool/src/pstatserver/pStatTimeline.cxx | 664 ++++++++++++++ pandatool/src/pstatserver/pStatTimeline.h | 130 +++ pandatool/src/pstatserver/pStatView.cxx | 19 +- pandatool/src/win-stats/CMakeLists.txt | 2 + pandatool/src/win-stats/winStatsChartMenu.cxx | 115 ++- .../src/win-stats/winStatsFlameGraph.cxx | 260 ++++-- pandatool/src/win-stats/winStatsFlameGraph.h | 13 +- pandatool/src/win-stats/winStatsGraph.cxx | 137 ++- pandatool/src/win-stats/winStatsGraph.h | 19 + pandatool/src/win-stats/winStatsLabel.cxx | 22 +- pandatool/src/win-stats/winStatsMonitor.I | 10 +- pandatool/src/win-stats/winStatsMonitor.cxx | 308 ++++++- pandatool/src/win-stats/winStatsMonitor.h | 21 +- pandatool/src/win-stats/winStatsPianoRoll.cxx | 90 +- pandatool/src/win-stats/winStatsPianoRoll.h | 7 +- .../src/win-stats/winStatsStripChart.cxx | 117 ++- pandatool/src/win-stats/winStatsStripChart.h | 3 + pandatool/src/win-stats/winStatsTimeline.cxx | 752 ++++++++++++++++ pandatool/src/win-stats/winStatsTimeline.h | 89 ++ .../src/win-stats/winstats_composite1.cxx | 1 + 53 files changed, 5310 insertions(+), 741 deletions(-) create mode 100644 pandatool/src/gtk-stats/gtkStatsTimeline.cxx create mode 100644 pandatool/src/gtk-stats/gtkStatsTimeline.h create mode 100644 pandatool/src/pstatserver/pStatTimeline.I create mode 100644 pandatool/src/pstatserver/pStatTimeline.cxx create mode 100644 pandatool/src/pstatserver/pStatTimeline.h create mode 100644 pandatool/src/win-stats/winStatsTimeline.cxx create mode 100644 pandatool/src/win-stats/winStatsTimeline.h diff --git a/pandatool/src/gtk-stats/CMakeLists.txt b/pandatool/src/gtk-stats/CMakeLists.txt index f44270de6a..68a10d392f 100644 --- a/pandatool/src/gtk-stats/CMakeLists.txt +++ b/pandatool/src/gtk-stats/CMakeLists.txt @@ -26,6 +26,7 @@ set(GTKSTATS_SOURCES gtkStatsPianoRoll.cxx gtkStatsServer.cxx gtkStatsStripChart.cxx + gtkStatsTimeline.cxx ) composite_sources(gtk-stats GTKSTATS_SOURCES) diff --git a/pandatool/src/gtk-stats/gtkStatsChartMenu.cxx b/pandatool/src/gtk-stats/gtkStatsChartMenu.cxx index 3c23398962..e610eb710f 100644 --- a/pandatool/src/gtk-stats/gtkStatsChartMenu.cxx +++ b/pandatool/src/gtk-stats/gtkStatsChartMenu.cxx @@ -88,7 +88,25 @@ do_update() { // Now rebuild the menu with the new set of entries. - // The menu item(s) for the thread's frame time goes first. + if (_thread_index == 0) { + // Timeline goes first. + GtkStatsMonitor::MenuDef smd(_thread_index, -1, GtkStatsMonitor::CT_timeline, false); + const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd); + + GtkWidget *menu_item = gtk_menu_item_new_with_label("Timeline"); + gtk_widget_show(menu_item); + gtk_menu_shell_append(GTK_MENU_SHELL(_menu), menu_item); + + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(GtkStatsMonitor::menu_activate), + (void *)menu_def); + + GtkWidget *sep = gtk_separator_menu_item_new(); + gtk_widget_show(sep); + gtk_menu_shell_append(GTK_MENU_SHELL(_menu), sep); + } + + // The menu item(s) for the thread's frame time goes second. add_view(_menu, view.get_top_level(), false); bool needs_separator = true; @@ -116,33 +134,22 @@ do_update() { } } - // Also menu items for flame graph and piano roll (following a separator). + // Also menu item for piano roll (following a separator). GtkWidget *sep = gtk_separator_menu_item_new(); gtk_widget_show(sep); gtk_menu_shell_append(GTK_MENU_SHELL(_menu), sep); { - GtkStatsMonitor::MenuDef smd(_thread_index, -2, false); - const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd); - - GtkWidget *menu_item = gtk_menu_item_new_with_label("Flame Graph"); - gtk_widget_show(menu_item); - gtk_menu_shell_append(GTK_MENU_SHELL(_menu), menu_item); - - g_signal_connect_swapped(G_OBJECT(menu_item), "activate", - G_CALLBACK(handle_menu), (void *)(const void *)menu_def); - } - - { - GtkStatsMonitor::MenuDef smd(_thread_index, -1, false); + GtkStatsMonitor::MenuDef smd(_thread_index, -1, GtkStatsMonitor::CT_piano_roll, false); const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd); GtkWidget *menu_item = gtk_menu_item_new_with_label("Piano Roll"); gtk_widget_show(menu_item); gtk_menu_shell_append(GTK_MENU_SHELL(_menu), menu_item); - g_signal_connect_swapped(G_OBJECT(menu_item), "activate", - G_CALLBACK(handle_menu), (void *)(const void *)menu_def); + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(GtkStatsMonitor::menu_activate), + (void *)menu_def); } } @@ -158,64 +165,83 @@ add_view(GtkWidget *parent_menu, const PStatViewLevel *view_level, const PStatClientData *client_data = _monitor->get_client_data(); std::string collector_name = client_data->get_collector_name(collector); - GtkStatsMonitor::MenuDef smd(_thread_index, collector, show_level); - const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd); - - GtkWidget *menu_item = gtk_menu_item_new_with_label(collector_name.c_str()); - gtk_widget_show(menu_item); - gtk_menu_shell_append(GTK_MENU_SHELL(parent_menu), menu_item); - - g_signal_connect_swapped(G_OBJECT(menu_item), "activate", - G_CALLBACK(handle_menu), (void *)(const void *)menu_def); - int num_children = view_level->get_num_children(); - if (num_children > 1) { - // If the collector has more than one child, add a menu entry to go - // directly to each of its children. - std::string submenu_name = collector_name + " components"; + if (show_level && num_children == 0) { + // For a level collector without children, no point in making a submenu. + GtkStatsMonitor::MenuDef smd(_thread_index, collector, GtkStatsMonitor::CT_strip_chart, show_level); + const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd); - GtkWidget *submenu_item = gtk_menu_item_new_with_label(submenu_name.c_str()); + GtkWidget *menu_item = gtk_menu_item_new_with_label(collector_name.c_str()); + gtk_widget_show(menu_item); + gtk_menu_shell_append(GTK_MENU_SHELL(parent_menu), menu_item); + + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(GtkStatsMonitor::menu_activate), + (void *)menu_def); + return; + } + + GtkWidget *menu; + if (!show_level && collector == 0 && num_children == 0) { + // Root collector without children, just add the options directly to the + // parent menu. + menu = parent_menu; + } + else { + // Create a submenu. + GtkWidget *submenu_item = gtk_menu_item_new_with_label(collector_name.c_str()); gtk_widget_show(submenu_item); gtk_menu_shell_append(GTK_MENU_SHELL(parent_menu), submenu_item); - GtkWidget *submenu = gtk_menu_new(); - gtk_widget_show(submenu); - gtk_menu_item_set_submenu(GTK_MENU_ITEM(submenu_item), submenu); + menu = gtk_menu_new(); + gtk_widget_show(menu); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(submenu_item), menu); + } + + { + GtkStatsMonitor::MenuDef smd(_thread_index, collector, GtkStatsMonitor::CT_strip_chart, show_level); + const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd); + + GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Strip Chart"); + gtk_widget_show(menu_item); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(GtkStatsMonitor::menu_activate), + (void *)menu_def); + } + + if (!show_level) { + if (collector == 0 && num_children == 0) { + collector = -1; + } + + GtkStatsMonitor::MenuDef smd(_thread_index, collector, GtkStatsMonitor::CT_flame_graph, show_level); + const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd); + + GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Flame Graph"); + gtk_widget_show(menu_item); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(GtkStatsMonitor::menu_activate), + (void *)menu_def); + } + + if (num_children > 0) { + GtkWidget *sep = gtk_separator_menu_item_new(); + gtk_widget_show(sep); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), sep); // Reverse the order since the menus are listed from the top down; we want // to be visually consistent with the graphs, which list these labels from // the bottom up. for (int c = num_children - 1; c >= 0; c--) { - add_view(submenu, view_level->get_child(c), show_level); + add_view(menu, view_level->get_child(c), show_level); } } } -/** - * Callback when a menu item is selected. - */ -void GtkStatsChartMenu:: -handle_menu(gpointer data) { - const GtkStatsMonitor::MenuDef *menu_def = (GtkStatsMonitor::MenuDef *)data; - GtkStatsMonitor *monitor = menu_def->_monitor; - - if (monitor == nullptr) { - return; - } - - if (menu_def->_collector_index == -2) { - monitor->open_flame_graph(menu_def->_thread_index); - } - else if (menu_def->_collector_index < 0) { - monitor->open_piano_roll(menu_def->_thread_index); - } - else { - monitor->open_strip_chart(menu_def->_thread_index, - menu_def->_collector_index, - menu_def->_show_level); - } -} - /** * Removes a previous menu child from the menu. */ diff --git a/pandatool/src/gtk-stats/gtkStatsChartMenu.h b/pandatool/src/gtk-stats/gtkStatsChartMenu.h index 6206d951ec..41e21aa3ee 100644 --- a/pandatool/src/gtk-stats/gtkStatsChartMenu.h +++ b/pandatool/src/gtk-stats/gtkStatsChartMenu.h @@ -40,7 +40,6 @@ private: void add_view(GtkWidget *parent_menu, const PStatViewLevel *view_level, bool show_level); - static void handle_menu(gpointer data); static void remove_menu_child(GtkWidget *widget, gpointer data); GtkStatsMonitor *_monitor; diff --git a/pandatool/src/gtk-stats/gtkStatsFlameGraph.cxx b/pandatool/src/gtk-stats/gtkStatsFlameGraph.cxx index bb52f1374c..d5ecdb0d29 100644 --- a/pandatool/src/gtk-stats/gtkStatsFlameGraph.cxx +++ b/pandatool/src/gtk-stats/gtkStatsFlameGraph.cxx @@ -25,11 +25,8 @@ static const int default_flame_graph_height = 150; GtkStatsFlameGraph:: GtkStatsFlameGraph(GtkStatsMonitor *monitor, int thread_index, int collector_index) : - PStatFlameGraph(monitor, monitor->get_view(thread_index), - thread_index, collector_index, - default_flame_graph_width, - default_flame_graph_height), - GtkStatsGraph(monitor) + PStatFlameGraph(monitor, thread_index, collector_index, 0, 0), + GtkStatsGraph(monitor, false) { // Let's show the units on the guide bar labels. There's room. set_guide_bar_units(get_guide_bar_units() | GBU_show_units); @@ -54,13 +51,9 @@ GtkStatsFlameGraph(GtkStatsMonitor *monitor, int thread_index, gtk_box_pack_start(GTK_BOX(_top_hbox), _scale_area, TRUE, TRUE, 0); gtk_box_pack_end(GTK_BOX(_top_hbox), _total_label, FALSE, FALSE, 0); - gtk_widget_set_size_request(_graph_window, default_flame_graph_width, - default_flame_graph_height); - - // Add a fixed container to the overlay to allow arbitrary positioning - // of labels therein. - _fixed = gtk_fixed_new(); - gtk_overlay_add_overlay(GTK_OVERLAY(_graph_overlay), _fixed); + gtk_widget_set_size_request(_graph_window, + default_flame_graph_width * monitor->get_resolution() / 96, + default_flame_graph_height * monitor->get_resolution() / 96); gtk_widget_show_all(_window); gtk_widget_show(_window); @@ -71,6 +64,10 @@ GtkStatsFlameGraph(GtkStatsMonitor *monitor, int thread_index, gtk_widget_set_size_request(_window, 0, 0); clear_region(); + + if (get_average_mode()) { + start_animation(); + } } /** @@ -119,7 +116,9 @@ new_data(int thread_index, int frame_number) { */ void GtkStatsFlameGraph:: force_redraw() { - PStatFlameGraph::force_redraw(); + if (_cr) { + PStatFlameGraph::force_redraw(); + } } /** @@ -152,34 +151,7 @@ set_time_units(int unit_mask) { */ void GtkStatsFlameGraph:: on_click_label(int collector_index) { - int prev_collector_index = get_collector_index(); - if (collector_index == prev_collector_index && collector_index != 0) { - // Clicking on the top label means to go up to the parent level. - const PStatClientData *client_data = - GtkStatsGraph::_monitor->get_client_data(); - if (client_data->has_collector(collector_index)) { - const PStatCollectorDef &def = - client_data->get_collector_def(collector_index); - collector_index = def._parent_index; - set_collector_index(collector_index); - } - } - else { - // Clicking on any other label means to focus on that. - set_collector_index(collector_index); - } - - // Change the root collector to show the full name. - if (prev_collector_index != collector_index) { - auto it = _labels.find(prev_collector_index); - if (it != _labels.end()) { - it->second->update_text(false); - } - it = _labels.find(collector_index); - if (it != _labels.end()) { - it->second->update_text(true); - } - } + set_collector_index(collector_index); } /** @@ -189,6 +161,10 @@ void GtkStatsFlameGraph:: on_enter_label(int collector_index) { if (collector_index != _highlighted_index) { _highlighted_index = collector_index; + + if (!get_average_mode()) { + PStatFlameGraph::force_redraw(); + } } } @@ -199,54 +175,11 @@ void GtkStatsFlameGraph:: on_leave_label(int collector_index) { if (collector_index == _highlighted_index && collector_index != -1) { _highlighted_index = -1; - } -} -/** - * Called when the mouse hovers over a label, and should return the text that - * should appear on the tooltip. - */ -std::string GtkStatsFlameGraph:: -get_label_tooltip(int collector_index) const { - return PStatFlameGraph::get_label_tooltip(collector_index); -} - -/** - * Repositions the labels. - */ -void GtkStatsFlameGraph:: -update_labels() { - PStatFlameGraph::update_labels(); -} - -/** - * Repositions a label. If width is 0, the label should be deleted. - */ -void GtkStatsFlameGraph:: -update_label(int collector_index, int row, int x, int width) { - GtkStatsLabel *label; - - auto it = _labels.find(collector_index); - if (it != _labels.end()) { - label = it->second; - if (width == 0) { - gtk_container_remove(GTK_CONTAINER(_fixed), label->get_widget()); - delete label; - _labels.erase(it); - return; + if (!get_average_mode()) { + PStatFlameGraph::force_redraw(); } - gtk_fixed_move(GTK_FIXED(_fixed), label->get_widget(), x, _ysize - (row + 1) * label->get_height()); } - else { - if (width == 0) { - return; - } - label = new GtkStatsLabel(GtkStatsGraph::_monitor, this, _thread_index, collector_index, false, false); - _labels[collector_index] = label; - gtk_fixed_put(GTK_FIXED(_fixed), label->get_widget(), x, _ysize - (row + 1) * label->get_height()); - } - - gtk_widget_set_size_request(label->get_widget(), std::min(width, _xsize), label->get_height()); } /** @@ -255,8 +188,7 @@ update_label(int collector_index, int row, int x, int width) { void GtkStatsFlameGraph:: normal_guide_bars() { // We want vaguely 100 pixels between guide bars. - double res = gdk_screen_get_resolution(gdk_screen_get_default()); - int num_bars = (int)(get_xsize() / (100.0 * (res > 0 ? res / 96.0 : 1.0))); + int num_bars = get_xsize() / (_pixel_scale * 25); _guide_bars.clear(); @@ -292,6 +224,63 @@ begin_draw() { } } +/** + * Should be overridden by the user class. Should draw a single bar at the + * indicated location. + */ +void GtkStatsFlameGraph:: +draw_bar(int depth, int from_x, int to_x, int collector_index) { + double bottom = get_ysize() - depth * _pixel_scale * 5; + double top = bottom - _pixel_scale * 5; + + bool is_highlighted = collector_index == _highlighted_index; + cairo_set_source(_cr, get_collector_pattern(collector_index, is_highlighted)); + + if (to_x < from_x + 3) { + // It's just a tiny sliver. This is a more reliable way to draw it. + cairo_rectangle(_cr, from_x, top, to_x - from_x, bottom - top); + cairo_fill(_cr); + } + else { + double radius = std::min((double)_pixel_scale, (to_x - from_x) / 2.0); + cairo_new_sub_path(_cr); + cairo_arc(_cr, to_x - radius, top + radius, radius, -0.5 * M_PI, 0.0); + cairo_arc(_cr, to_x - radius, bottom - radius, radius, 0.0, 0.5 * M_PI); + cairo_arc(_cr, from_x + radius, bottom - radius, radius, 0.5 * M_PI, M_PI); + cairo_arc(_cr, from_x + radius, top + radius, radius, M_PI, 1.5 * M_PI); + cairo_close_path(_cr); + cairo_fill(_cr); + + if ((to_x - from_x) >= _pixel_scale * 4) { + // Only bother drawing the text if we've got some space to draw on. + int left = std::max(from_x, 0) + _pixel_scale / 2; + int right = std::min(to_x, get_xsize()) - _pixel_scale / 2; + + const PStatClientData *client_data = GtkStatsGraph::_monitor->get_client_data(); + const std::string &name = client_data->get_collector_name(collector_index); + + // Choose a suitable foreground color. + LRGBColor fg = get_collector_text_color(collector_index, is_highlighted); + cairo_set_source_rgb(_cr, fg[0], fg[1], fg[2]); + + PangoLayout *layout = gtk_widget_create_pango_layout(_graph_window, name.c_str()); + pango_layout_set_attributes(layout, _pango_attrs); + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); + pango_layout_set_width(layout, (right - left) * PANGO_SCALE); + pango_layout_set_height(layout, -1); + + int width, height; + pango_layout_get_pixel_size(layout, &width, &height); + + // Center the text vertically in the bar. + cairo_move_to(_cr, left, top + (bottom - top - height) / 2); + pango_cairo_show_layout(_cr, layout); + + g_object_unref(layout); + } + } +} + /** * Called after all the bars have been drawn, this triggers a refresh event to * draw it to the window. @@ -308,6 +297,15 @@ void GtkStatsFlameGraph:: idle() { } +/** + * Overridden by a derived class to implement an animation. If it returns + * false, the animation timer is stopped. + */ +bool GtkStatsFlameGraph:: +animate(double time, double dt) { + return PStatFlameGraph::animate(time, dt); +} + /** * This is called during the servicing of the draw event; it gives a derived * class opportunity to do some further painting into the graph window. @@ -320,6 +318,15 @@ additional_graph_window_paint(cairo_t *cr) { } } +/** + * Called when the mouse hovers over the graph, and should return the text that + * should appear on the tooltip. + */ +std::string GtkStatsFlameGraph:: +get_graph_tooltip(int mouse_x, int mouse_y) const { + return get_bar_tooltip(pixel_to_depth(mouse_y), mouse_x); +} + /** * Based on the mouse position within the window's client area, look for * draggable things the mouse might be hovering over and return the @@ -352,12 +359,74 @@ consider_drag_start(int graph_x, int graph_y) { * Called when the mouse button is depressed within the graph window. */ gboolean GtkStatsFlameGraph:: -handle_button_press(GtkWidget *widget, int graph_x, int graph_y, - bool double_click) { +handle_button_press(int graph_x, int graph_y, bool double_click, int button) { if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) { - if (double_click) { - // Clicking on whitespace in the graph goes to the parent. - on_click_label(get_collector_index()); + int depth = pixel_to_depth(graph_y); + int collector_index = get_bar_collector(depth, graph_x); + if (button == 3) { + if (collector_index >= 0) { + GtkWidget *menu = gtk_menu_new(); + _popup_index = collector_index; + + std::string label = get_bar_tooltip(depth, graph_x); + if (!label.empty()) { + GtkWidget *menu_item = gtk_menu_item_new_with_label(label.c_str()); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + gtk_widget_set_sensitive(menu_item, FALSE); + } + + { + GtkWidget *menu_item = gtk_menu_item_new_with_label("Set as Focus"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + + if (collector_index == 0 && get_collector_index() == 0) { + gtk_widget_set_sensitive(menu_item, FALSE); + } else { + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(+[] (GtkWidget *widget, gpointer data) { + GtkStatsFlameGraph *self = (GtkStatsFlameGraph *)data; + self->set_collector_index(self->_popup_index); + }), + this); + } + } + + { + const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({ + get_thread_index(), collector_index, + GtkStatsMonitor::CT_strip_chart, false, + }); + + GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Strip Chart"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(GtkStatsMonitor::menu_activate), + (void *)menu_def); + } + + { + const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({ + get_thread_index(), collector_index, + GtkStatsMonitor::CT_flame_graph, + }); + + GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Flame Graph"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(GtkStatsMonitor::menu_activate), + (void *)menu_def); + } + + gtk_widget_show_all(menu); + gtk_menu_popup_at_pointer(GTK_MENU(menu), nullptr); + return TRUE; + } + return FALSE; + } + else if (double_click && button == 1) { + // Double-clicking on a color bar in the graph will zoom the graph into + // that collector. + set_collector_index(collector_index); return TRUE; } } @@ -375,21 +444,21 @@ handle_button_press(GtkWidget *widget, int graph_x, int graph_y, return TRUE; } - return GtkStatsGraph::handle_button_press(widget, graph_x, graph_y, - double_click); + return GtkStatsGraph::handle_button_press(graph_x, graph_y, + double_click, button); } /** * Called when the mouse button is released within the graph window. */ gboolean GtkStatsFlameGraph:: -handle_button_release(GtkWidget *widget, int graph_x, int graph_y) { +handle_button_release(int graph_x, int graph_y) { if (_drag_mode == DM_scale) { set_drag_mode(DM_none); // ReleaseCapture(); - return handle_motion(widget, graph_x, graph_y); - - } else if (_drag_mode == DM_guide_bar) { + return handle_motion(graph_x, graph_y); + } + else if (_drag_mode == DM_guide_bar) { if (graph_x < 0 || graph_x >= get_xsize()) { remove_user_guide_bar(_drag_guide_bar); } else { @@ -397,17 +466,30 @@ handle_button_release(GtkWidget *widget, int graph_x, int graph_y) { } set_drag_mode(DM_none); // ReleaseCapture(); - return handle_motion(widget, graph_x, graph_y); + return handle_motion(graph_x, graph_y); } - return GtkStatsGraph::handle_button_release(widget, graph_x, graph_y); + return GtkStatsGraph::handle_button_release(graph_x, graph_y); } /** * Called when the mouse is moved within the graph window. */ gboolean GtkStatsFlameGraph:: -handle_motion(GtkWidget *widget, int graph_x, int graph_y) { +handle_motion(int graph_x, int graph_y) { + if (_drag_mode == DM_none && _potential_drag_mode == DM_none && + graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) { + // When the mouse is over a color bar, highlight it. + int depth = pixel_to_depth(graph_y); + int collector_index = get_bar_collector(depth, graph_x); + on_enter_label(collector_index); + } + else { + // If the mouse is in some drag mode, stop highlighting. + _label_stack.highlight_label(-1); + on_leave_label(_highlighted_index); + } + if (_drag_mode == DM_new_guide_bar) { // We haven't created the new guide bar yet; we won't until the mouse // comes within the graph's region. @@ -422,7 +504,25 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) { return TRUE; } - return GtkStatsGraph::handle_motion(widget, graph_x, graph_y); + return GtkStatsGraph::handle_motion(graph_x, graph_y); +} + +/** + * Called when the mouse has left the graph window. + */ +gboolean GtkStatsFlameGraph:: +handle_leave() { + _label_stack.highlight_label(-1); + on_leave_label(_highlighted_index); + return TRUE; +} + +/** + * Converts a pixel to a depth index. + */ +int GtkStatsFlameGraph:: +pixel_to_depth(int y) const { + return (get_ysize() - 1 - y) / (_pixel_scale * 5); } /** @@ -443,7 +543,7 @@ draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar) { cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]); break; - case GBS_normal: + default: cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]); break; } @@ -484,7 +584,7 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) { cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]); break; - case GBS_normal: + default: cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]); break; } @@ -492,13 +592,13 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) { int x = height_to_pixel(bar._height); const std::string &label = bar._label; - PangoLayout *layout = gtk_widget_create_pango_layout(_window, label.c_str()); + PangoLayout *layout = gtk_widget_create_pango_layout(_scale_area, label.c_str()); int width, height; pango_layout_get_pixel_size(layout, &width, &height); if (bar._style != GBS_user) { - double from_height = pixel_to_height(x - width); - double to_height = pixel_to_height(x + width); + double from_height = pixel_to_height(x - width * _cr_scale); + double to_height = pixel_to_height(x + width * _cr_scale); if (find_user_guide_bar(from_height, to_height) >= 0) { // Omit the label: there's a user-defined guide bar in the same space. g_object_unref(layout); @@ -510,6 +610,8 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) { // Now convert our x to a coordinate within our drawing area. int junk_y; + x /= _cr_scale; + // The x coordinate comes from the graph_window. gtk_widget_translate_coordinates(_graph_window, _scale_area, x, 0, @@ -537,6 +639,9 @@ toggled_callback(GtkToggleButton *button, gpointer data) { bool active = gtk_toggle_button_get_active(button); self->set_average_mode(active); + if (active) { + self->start_animation(); + } } /** diff --git a/pandatool/src/gtk-stats/gtkStatsFlameGraph.h b/pandatool/src/gtk-stats/gtkStatsFlameGraph.h index f6e57c3bce..ad550d16ac 100644 --- a/pandatool/src/gtk-stats/gtkStatsFlameGraph.h +++ b/pandatool/src/gtk-stats/gtkStatsFlameGraph.h @@ -28,7 +28,7 @@ class GtkStatsLabel; class GtkStatsFlameGraph : public PStatFlameGraph, public GtkStatsGraph { public: GtkStatsFlameGraph(GtkStatsMonitor *monitor, int thread_index, - int collector_index=0); + int collector_index=-1); virtual ~GtkStatsFlameGraph(); virtual void new_collector(int collector_index); @@ -40,27 +40,30 @@ public: virtual void on_click_label(int collector_index); virtual void on_enter_label(int collector_index); virtual void on_leave_label(int collector_index); - virtual std::string get_label_tooltip(int collector_index) const; protected: - virtual void update_labels(); - virtual void update_label(int collector_index, int row, int x, int width); virtual void normal_guide_bars(); void clear_region(); virtual void begin_draw(); + virtual void draw_bar(int depth, int from_x, int to_x, int collector_index); virtual void end_draw(); virtual void idle(); + virtual bool animate(double time, double dt); + virtual void additional_graph_window_paint(cairo_t *cr); + virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const; virtual DragMode consider_drag_start(int graph_x, int graph_y); - virtual gboolean handle_button_press(GtkWidget *widget, int graph_x, int graph_y, - bool double_click); - virtual gboolean handle_button_release(GtkWidget *widget, int graph_x, int graph_y); - virtual gboolean handle_motion(GtkWidget *widget, int graph_x, int graph_y); + virtual gboolean handle_button_press(int graph_x, int graph_y, + bool double_click, int button); + virtual gboolean handle_button_release(int graph_x, int graph_y); + virtual gboolean handle_motion(int graph_x, int graph_y); + virtual gboolean handle_leave(); private: + int pixel_to_depth(int y) const; void draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar); void draw_guide_labels(cairo_t *cr); void draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar); @@ -70,12 +73,12 @@ private: private: std::string _net_value_text; - pmap _labels; GtkWidget *_top_hbox; GtkWidget *_average_check_box; GtkWidget *_total_label; - GtkWidget *_fixed; + + int _popup_index = -1; }; #endif diff --git a/pandatool/src/gtk-stats/gtkStatsGraph.cxx b/pandatool/src/gtk-stats/gtkStatsGraph.cxx index 2734498b12..269b8ec47f 100644 --- a/pandatool/src/gtk-stats/gtkStatsGraph.cxx +++ b/pandatool/src/gtk-stats/gtkStatsGraph.cxx @@ -30,7 +30,7 @@ const double GtkStatsGraph::rgb_user_guide_bar[3] = { * */ GtkStatsGraph:: -GtkStatsGraph(GtkStatsMonitor *monitor) : +GtkStatsGraph(GtkStatsMonitor *monitor, bool has_label_stack) : _monitor(monitor) { _parent_window = nullptr; @@ -40,21 +40,25 @@ GtkStatsGraph(GtkStatsMonitor *monitor) : GtkWidget *parent_window = monitor->get_window(); - GdkDisplay *display = gdk_window_get_display(gtk_widget_get_window(parent_window)); + GdkWindow *window = gtk_widget_get_window(parent_window); + GdkDisplay *display = gdk_window_get_display(window); _hand_cursor = gdk_cursor_new_for_display(display, GDK_HAND2); + int scale = gdk_window_get_scale_factor(window); + _pixel_scale = scale * monitor->get_resolution() * 4 / 96; + _cr_surface = nullptr; _cr = nullptr; + _cr_scale = scale; + _pango_attrs = nullptr; _surface_xsize = 0; _surface_ysize = 0; _window = gtk_window_new(GTK_WINDOW_TOPLEVEL); - - // These calls were intended to kind of emulate the Windows MDI behavior, - // but it's just weird. gtk_window_set_transient_for(GTK_WINDOW(_window), - // GTK_WINDOW(parent_window)); - // gtk_window_set_destroy_with_parent(GTK_WINDOW(_window), TRUE); + gtk_window_set_type_hint(GTK_WINDOW(_window), GDK_WINDOW_TYPE_HINT_UTILITY); + //gtk_window_set_transient_for(GTK_WINDOW(_window), GTK_WINDOW(parent_window)); + //gtk_window_set_position(GTK_WINDOW(_window), GTK_WIN_POS_CENTER_ON_PARENT); gtk_widget_add_events(_window, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | @@ -63,17 +67,17 @@ GtkStatsGraph(GtkStatsMonitor *monitor) : G_CALLBACK(window_delete_event), this); g_signal_connect(G_OBJECT(_window), "destroy", G_CALLBACK(window_destroy), this); - g_signal_connect(G_OBJECT(_window), "button_press_event", - G_CALLBACK(button_press_event_callback), this); - g_signal_connect(G_OBJECT(_window), "button_release_event", - G_CALLBACK(button_release_event_callback), this); - g_signal_connect(G_OBJECT(_window), "motion_notify_event", - G_CALLBACK(motion_notify_event_callback), this); + //g_signal_connect(G_OBJECT(_window), "button_press_event", + // G_CALLBACK(button_press_event_callback), this); + //g_signal_connect(G_OBJECT(_window), "button_release_event", + // G_CALLBACK(button_release_event_callback), this); + //g_signal_connect(G_OBJECT(_window), "motion_notify_event", + // G_CALLBACK(motion_notify_event_callback), this); _graph_window = gtk_drawing_area_new(); gtk_widget_add_events(_graph_window, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | - GDK_POINTER_MOTION_MASK); + GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK); g_signal_connect(G_OBJECT(_graph_window), "draw", G_CALLBACK(graph_draw_callback), this); g_signal_connect(G_OBJECT(_graph_window), "configure_event", @@ -84,37 +88,41 @@ GtkStatsGraph(GtkStatsMonitor *monitor) : G_CALLBACK(button_release_event_callback), this); g_signal_connect(G_OBJECT(_graph_window), "motion_notify_event", G_CALLBACK(motion_notify_event_callback), this); + g_signal_connect(G_OBJECT(_graph_window), "leave_notify_event", + G_CALLBACK(leave_notify_event_callback), this); + g_signal_connect(G_OBJECT(_graph_window), "query-tooltip", + G_CALLBACK(query_tooltip_callback), this); - // An overlay inside the frame, for charts that want to display widgets on - // top of the graph. - _graph_overlay = gtk_overlay_new(); - gtk_container_add(GTK_CONTAINER(_graph_overlay), _graph_window); + gtk_widget_set_has_tooltip(_graph_window, TRUE); // A Frame to hold the graph. _graph_frame = gtk_frame_new(nullptr); gtk_frame_set_shadow_type(GTK_FRAME(_graph_frame), GTK_SHADOW_IN); - gtk_container_add(GTK_CONTAINER(_graph_frame), _graph_overlay); + gtk_container_add(GTK_CONTAINER(_graph_frame), _graph_window); // A VBox to hold the graph's frame, and any numbers (scale legend? total?) // above it. _graph_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - gtk_box_pack_end(GTK_BOX(_graph_vbox), _graph_frame, - TRUE, TRUE, 0); + gtk_box_pack_end(GTK_BOX(_graph_vbox), _graph_frame, TRUE, TRUE, 0); // An HBox to hold the graph's frame, and the scale legend to the right of // it. _graph_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); - gtk_box_pack_start(GTK_BOX(_graph_hbox), _graph_vbox, - TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(_graph_hbox), _graph_vbox, TRUE, TRUE, 0); // An HPaned to hold the label stack and the graph hbox. - _hpaned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); - gtk_paned_set_wide_handle(GTK_PANED(_hpaned), TRUE); - gtk_container_add(GTK_CONTAINER(_window), _hpaned); - gtk_container_set_border_width(GTK_CONTAINER(_window), 8); + if (has_label_stack) { + _hpaned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); + gtk_paned_set_wide_handle(GTK_PANED(_hpaned), TRUE); + gtk_container_add(GTK_CONTAINER(_window), _hpaned); + gtk_container_set_border_width(GTK_CONTAINER(_window), 8); - gtk_paned_pack1(GTK_PANED(_hpaned), _label_stack.get_widget(), FALSE, FALSE); - gtk_paned_pack2(GTK_PANED(_hpaned), _graph_hbox, TRUE, TRUE); + gtk_paned_pack1(GTK_PANED(_hpaned), _label_stack.get_widget(), FALSE, FALSE); + gtk_paned_pack2(GTK_PANED(_hpaned), _graph_hbox, TRUE, TRUE); + } + else { + gtk_container_add(GTK_CONTAINER(_window), _graph_hbox); + } _drag_mode = DM_none; _potential_drag_mode = DM_none; @@ -128,6 +136,11 @@ GtkStatsGraph(GtkStatsMonitor *monitor) : */ GtkStatsGraph:: ~GtkStatsGraph() { + if (_timer_id != 0) { + gtk_widget_remove_tick_callback(_graph_window, _timer_id); + _timer_id = 0; + } + _monitor = nullptr; release_surface(); @@ -136,9 +149,15 @@ GtkStatsGraph:: cairo_pattern_destroy(item.second.second); } _brushes.clear(); + _text_colors.clear(); _label_stack.clear_labels(); + if (_pango_attrs != nullptr) { + pango_attr_list_unref(_pango_attrs); + _pango_attrs = nullptr; + } + if (_window != nullptr) { GtkWidget *window = _window; _window = nullptr; @@ -211,6 +230,13 @@ void GtkStatsGraph:: on_click_label(int collector_index) { } +/** + * Called when a pop-up menu should be shown for the label. + */ +void GtkStatsGraph:: +on_popup_label(int collector_index) { +} + /** * Called when the user hovers the mouse over a label. */ @@ -260,6 +286,29 @@ close() { } } +/** + * Turns on the animation timer, if it hasn't already been turned on. + */ +void GtkStatsGraph:: +start_animation() { + if (_timer_id != 0) { + return; + } + + _time = 0; + _timer_id = gtk_widget_add_tick_callback(_graph_window, tick_callback, + this, nullptr); +} + +/** + * Overridden by a derived class to implement an animation. If it returns + * false, the animation timer is stopped. + */ +bool GtkStatsGraph:: +animate(double time, double dt) { + return false; +} + /** * Returns a pattern suitable for drawing in the indicated collector's color. */ @@ -286,6 +335,29 @@ get_collector_pattern(int collector_index, bool highlight) { return highlight ? hpattern : pattern; } +/** + * Returns a text color suitable for the given collector. + */ +LRGBColor GtkStatsGraph:: +get_collector_text_color(int collector_index, bool highlight) { + TextColors::iterator tci; + tci = _text_colors.find(collector_index); + if (tci != _text_colors.end()) { + return highlight ? (*tci).second.second : (*tci).second.first; + } + + LRGBColor rgb = _monitor->get_collector_color(collector_index); + double bright = + rgb[0] * 0.2126 + + rgb[1] * 0.7152 + + rgb[2] * 0.0722; + LRGBColor color = bright >= 0.5 ? LRGBColor(0) : LRGBColor(1); + LRGBColor hcolor = bright * 0.75 >= 0.5 ? LRGBColor(0) : LRGBColor(1); + + _text_colors[collector_index] = std::make_pair(color, hcolor); + return highlight ? hcolor : color; +} + /** * This is called during the servicing of the draw event; it gives a derived * class opportunity to do some further painting into the graph window. @@ -294,6 +366,15 @@ void GtkStatsGraph:: additional_graph_window_paint(cairo_t *cr) { } +/** + * Called when the mouse hovers over the graph, and should return the text that + * should appear on the tooltip. + */ +std::string GtkStatsGraph:: +get_graph_tooltip(int mouse_x, int mouse_y) const { + return std::string(); +} + /** * Based on the mouse position within the graph window, look for draggable * things the mouse might be hovering over and return the appropriate DragMode @@ -318,9 +399,8 @@ set_drag_mode(GtkStatsGraph::DragMode drag_mode) { * window. */ gboolean GtkStatsGraph:: -handle_button_press(GtkWidget *widget, int graph_x, int graph_y, - bool double_click) { - if (_potential_drag_mode != DM_none) { +handle_button_press(int graph_x, int graph_y, bool double_click, int button) { + if (_potential_drag_mode != DM_none && button == 1) { set_drag_mode(_potential_drag_mode); _drag_start_x = graph_x; _drag_start_y = graph_y; @@ -334,18 +414,18 @@ handle_button_press(GtkWidget *widget, int graph_x, int graph_y, * window. */ gboolean GtkStatsGraph:: -handle_button_release(GtkWidget *widget, int graph_x, int graph_y) { +handle_button_release(int graph_x, int graph_y) { set_drag_mode(DM_none); // ReleaseCapture(); - return handle_motion(widget, graph_x, graph_y); + return handle_motion(graph_x, graph_y); } /** * Called when the mouse is moved within the window, or any nested window. */ gboolean GtkStatsGraph:: -handle_motion(GtkWidget *widget, int graph_x, int graph_y) { +handle_motion(int graph_x, int graph_y) { _potential_drag_mode = consider_drag_start(graph_x, graph_y); GdkWindow *window = gtk_widget_get_window(_window); @@ -353,29 +433,47 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) { if (_potential_drag_mode == DM_guide_bar || _drag_mode == DM_guide_bar) { gdk_window_set_cursor(window, _hand_cursor); - - } else { + } + else { gdk_window_set_cursor(window, nullptr); } return TRUE; } +/** + * Called when the mouse has left the graph window. + */ +gboolean GtkStatsGraph:: +handle_leave() { + return FALSE; +} + /** * Sets up a backing-store bitmap of the indicated size. */ void GtkStatsGraph:: -setup_surface(int xsize, int ysize) { +setup_surface(int xsize, int ysize, int scale) { release_surface(); - _surface_xsize = std::max(xsize, 0); - _surface_ysize = std::max(ysize, 0); + _surface_xsize = xsize; + _surface_ysize = ysize; + _pixel_scale = scale * _monitor->get_resolution() * 4 / 96; - _cr_surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, _surface_xsize, _surface_ysize); + GdkWindow *window = gtk_widget_get_window(_graph_window); + _cr_surface = gdk_window_create_similar_image_surface(window, CAIRO_FORMAT_RGB24, _surface_xsize, _surface_ysize, 1); _cr = cairo_create(_cr_surface); + _cr_scale = scale; cairo_set_source_rgb(_cr, 1.0, 1.0, 1.0); cairo_paint(_cr); + + // Cache the font scale attribute. + _pango_attrs = pango_attr_list_new(); + PangoAttribute *attr = pango_attr_scale_new(scale * 0.9); + attr->start_index = 0; + attr->end_index = -1; + pango_attr_list_insert(_pango_attrs, attr); } /** @@ -386,6 +484,13 @@ release_surface() { if (_cr_surface != nullptr) { cairo_surface_destroy(_cr_surface); cairo_destroy(_cr); + _cr_surface = nullptr; + _cr = nullptr; + } + + if (_pango_attrs != nullptr) { + pango_attr_list_unref(_pango_attrs); + _pango_attrs = nullptr; } } @@ -416,6 +521,8 @@ graph_draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) { GtkStatsGraph *self = (GtkStatsGraph *)data; if (self->_cr_surface != nullptr) { + double scale = 1.0 / self->_cr_scale; + cairo_scale(cr, scale, scale); cairo_set_source_surface(cr, self->_cr_surface, 0, 0); cairo_paint(cr); } @@ -430,12 +537,21 @@ graph_draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) { */ gboolean GtkStatsGraph:: configure_graph_callback(GtkWidget *widget, GdkEventConfigure *event, - gpointer data) { + gpointer data) { GtkStatsGraph *self = (GtkStatsGraph *)data; - self->changed_graph_size(event->width, event->height); - self->setup_surface(event->width, event->height); - self->force_redraw(); + GdkWindow *window = gtk_widget_get_window(widget); + int scale = gdk_window_get_scale_factor(window); + int scaled_xsize = std::max(event->width * scale, 0); + int scaled_ysize = std::max(event->height * scale, 0); + + if (self->_cr == nullptr || + self->_cr_scale != scale || + self->_surface_xsize != scaled_xsize || + self->_surface_ysize != scaled_ysize) { + self->setup_surface(scaled_xsize, scaled_ysize, scale); + self->changed_graph_size(scaled_xsize, scaled_ysize); + } return TRUE; } @@ -446,16 +562,19 @@ configure_graph_callback(GtkWidget *widget, GdkEventConfigure *event, */ gboolean GtkStatsGraph:: button_press_event_callback(GtkWidget *widget, GdkEventButton *event, - gpointer data) { + gpointer data) { GtkStatsGraph *self = (GtkStatsGraph *)data; int graph_x, graph_y; gtk_widget_translate_coordinates(widget, self->_graph_window, - (int)event->x, (int)event->y, - &graph_x, &graph_y); + (int)event->x, (int)event->y, + &graph_x, &graph_y); + graph_x *= self->_cr_scale; + graph_y *= self->_cr_scale; bool double_click = (event->type == GDK_2BUTTON_PRESS); - return self->handle_button_press(widget, graph_x, graph_y, double_click); + return self->handle_button_press(graph_x, graph_y, + double_click, event->button); } /** @@ -464,14 +583,16 @@ button_press_event_callback(GtkWidget *widget, GdkEventButton *event, */ gboolean GtkStatsGraph:: button_release_event_callback(GtkWidget *widget, GdkEventButton *event, - gpointer data) { + gpointer data) { GtkStatsGraph *self = (GtkStatsGraph *)data; int graph_x, graph_y; gtk_widget_translate_coordinates(widget, self->_graph_window, - (int)event->x, (int)event->y, - &graph_x, &graph_y); + (int)event->x, (int)event->y, + &graph_x, &graph_y); + graph_x *= self->_cr_scale; + graph_y *= self->_cr_scale; - return self->handle_button_release(widget, graph_x, graph_y); + return self->handle_button_release(graph_x, graph_y); } /** @@ -479,12 +600,65 @@ button_release_event_callback(GtkWidget *widget, GdkEventButton *event, */ gboolean GtkStatsGraph:: motion_notify_event_callback(GtkWidget *widget, GdkEventMotion *event, - gpointer data) { + gpointer data) { GtkStatsGraph *self = (GtkStatsGraph *)data; int graph_x, graph_y; gtk_widget_translate_coordinates(widget, self->_graph_window, - (int)event->x, (int)event->y, - &graph_x, &graph_y); + (int)event->x, (int)event->y, + &graph_x, &graph_y); + graph_x *= self->_cr_scale; + graph_y *= self->_cr_scale; - return self->handle_motion(widget, graph_x, graph_y); + return self->handle_motion(graph_x, graph_y); +} + +/** + * Called when the mouse has left the graph window. + */ +gboolean GtkStatsGraph:: +leave_notify_event_callback(GtkWidget *widget, GdkEventCrossing *event, + gpointer data) { + GtkStatsGraph *self = (GtkStatsGraph *)data; + return self->handle_leave(); +} + +/** + * Called when a tooltip should be displayed. + */ +gboolean GtkStatsGraph:: +query_tooltip_callback(GtkWidget *widget, gint x, gint y, + gboolean keyboard_tip, GtkTooltip *tooltip, + gpointer data) { + GtkStatsGraph *self = (GtkStatsGraph *)data; + x *= self->_cr_scale; + y *= self->_cr_scale; + + std::string text = self->get_graph_tooltip(x, y); + gtk_tooltip_set_text(tooltip, text.c_str()); + return !text.empty(); +} + +/** + * Called to update the animations. + */ +gboolean GtkStatsGraph:: +tick_callback(GtkWidget *widget, GdkFrameClock *clock, gpointer data) { + GtkStatsGraph *graph = (GtkStatsGraph *)data; + gint64 new_time = gdk_frame_clock_get_frame_time(clock); + if (graph->_time == 0) { + // First frame, so we don't have a dt yet. + graph->_time = new_time; + return TRUE; + } + gint64 delta = new_time - graph->_time; + if (delta == 0) { + return TRUE; + } + if (graph->animate(new_time / 1000000.0, delta / 1000000.0)) { + graph->_time = new_time; + return TRUE; + } else { + graph->_timer_id = 0; + return FALSE; + } } diff --git a/pandatool/src/gtk-stats/gtkStatsGraph.h b/pandatool/src/gtk-stats/gtkStatsGraph.h index 3a3f123faf..2ff48cecca 100644 --- a/pandatool/src/gtk-stats/gtkStatsGraph.h +++ b/pandatool/src/gtk-stats/gtkStatsGraph.h @@ -17,6 +17,7 @@ #include "pandatoolbase.h" #include "gtkStatsLabelStack.h" #include "pmap.h" +#include "luse.h" #include #include @@ -36,10 +37,11 @@ public: DM_guide_bar, DM_new_guide_bar, DM_sizing, + DM_pan, }; public: - GtkStatsGraph(GtkStatsMonitor *monitor); + GtkStatsGraph(GtkStatsMonitor *monitor, bool has_label_stack); virtual ~GtkStatsGraph(); virtual void new_collector(int collector_index); @@ -53,33 +55,43 @@ public: void user_guide_bars_changed(); virtual void on_click_label(int collector_index); + virtual void on_popup_label(int collector_index); virtual void on_enter_label(int collector_index); virtual void on_leave_label(int collector_index); virtual std::string get_label_tooltip(int collector_index) const; protected: void close(); + + void start_animation(); + virtual bool animate(double time, double dt); + cairo_pattern_t *get_collector_pattern(int collector_index, bool highlight = false); + LRGBColor get_collector_text_color(int collector_index, bool highlight = false); virtual void additional_graph_window_paint(cairo_t *cr); + virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const; virtual DragMode consider_drag_start(int graph_x, int graph_y); virtual void set_drag_mode(DragMode drag_mode); - virtual gboolean handle_button_press(GtkWidget *widget, int graph_x, int graph_y, - bool double_click); - virtual gboolean handle_button_release(GtkWidget *widget, int graph_x, int graph_y); - virtual gboolean handle_motion(GtkWidget *widget, int graph_x, int graph_y); + virtual gboolean handle_button_press(int graph_x, int graph_y, + bool double_click, int button); + virtual gboolean handle_button_release(int graph_x, int graph_y); + virtual gboolean handle_motion(int graph_x, int graph_y); + virtual gboolean handle_leave(); protected: // Table of patterns for our various collectors. typedef pmap > Brushes; Brushes _brushes; + typedef pmap > TextColors; + TextColors _text_colors; + GtkStatsMonitor *_monitor; GtkWidget *_parent_window; GtkWidget *_window; GtkWidget *_graph_frame; - GtkWidget *_graph_overlay; GtkWidget *_graph_window; GtkWidget *_graph_hbox; GtkWidget *_graph_vbox; @@ -92,6 +104,9 @@ protected: cairo_surface_t *_cr_surface; cairo_t *_cr; int _surface_xsize, _surface_ysize; + PangoAttrList *_pango_attrs; + int _cr_scale; + int _pixel_scale; DragMode _drag_mode; DragMode _potential_drag_mode; @@ -103,6 +118,9 @@ protected: bool _pause; + guint _timer_id = 0; + gint64 _time = 0; + static const double rgb_white[3]; static const double rgb_light_gray[3]; static const double rgb_dark_gray[3]; @@ -110,27 +128,36 @@ protected: static const double rgb_user_guide_bar[3]; private: - void setup_surface(int xsize, int ysize); + void setup_surface(int xsize, int ysize, int scale); void release_surface(); static gboolean window_delete_event(GtkWidget *widget, GdkEvent *event, - gpointer data); + gpointer data); static void window_destroy(GtkWidget *widget, gpointer data); static gboolean graph_draw_callback(GtkWidget *widget, - cairo_t *cr, gpointer data); + cairo_t *cr, gpointer data); static gboolean configure_graph_callback(GtkWidget *widget, - GdkEventConfigure *event, gpointer data); + GdkEventConfigure *event, + gpointer data); protected: static gboolean button_press_event_callback(GtkWidget *widget, - GdkEventButton *event, - gpointer data); + GdkEventButton *event, + gpointer data); static gboolean button_release_event_callback(GtkWidget *widget, - GdkEventButton *event, - gpointer data); + GdkEventButton *event, + gpointer data); static gboolean motion_notify_event_callback(GtkWidget *widget, - GdkEventMotion *event, - gpointer data); + GdkEventMotion *event, + gpointer data); + static gboolean leave_notify_event_callback(GtkWidget *widget, + GdkEventCrossing *event, + gpointer data); + static gboolean query_tooltip_callback(GtkWidget *widget, gint x, gint y, + gboolean keyboard_tip, + GtkTooltip *tooltip, gpointer data); + static gboolean tick_callback(GtkWidget *widget, GdkFrameClock *clock, + gpointer data); }; #endif diff --git a/pandatool/src/gtk-stats/gtkStatsLabel.cxx b/pandatool/src/gtk-stats/gtkStatsLabel.cxx index 9db4c8cd9c..7dae836b99 100644 --- a/pandatool/src/gtk-stats/gtkStatsLabel.cxx +++ b/pandatool/src/gtk-stats/gtkStatsLabel.cxx @@ -50,7 +50,6 @@ GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph, G_CALLBACK(query_tooltip_callback), this); gtk_widget_set_has_tooltip(_widget, TRUE); - gtk_widget_show(_widget); // Set the fg and bg colors on the label. LRGBColor rgb = _monitor->get_collector_color(_collector_index); @@ -71,7 +70,7 @@ GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph, } else { _fg_color = LRGBColor(1); } - if (bright >= 0.5 * 0.75) { + if (bright * 0.75 >= 0.5) { _highlight_fg_color = LRGBColor(0); } else { _highlight_fg_color = LRGBColor(1); @@ -81,6 +80,7 @@ GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph, _mouse_within = false; update_text(use_fullname); + gtk_widget_show_all(_widget); } /** @@ -237,7 +237,7 @@ enter_notify_event_callback(GtkWidget *widget, GdkEventCrossing *event, */ gboolean GtkStatsLabel:: leave_notify_event_callback(GtkWidget *widget, GdkEventCrossing *event, - gpointer data) { + gpointer data) { GtkStatsLabel *self = (GtkStatsLabel *)data; self->set_mouse_within(false); return TRUE; @@ -248,12 +248,14 @@ leave_notify_event_callback(GtkWidget *widget, GdkEventCrossing *event, */ gboolean GtkStatsLabel:: button_press_event_callback(GtkWidget *widget, GdkEventButton *event, - gpointer data) { + gpointer data) { GtkStatsLabel *self = (GtkStatsLabel *)data; - bool double_click = (event->type == GDK_2BUTTON_PRESS); - if (double_click) { + if (event->type == GDK_2BUTTON_PRESS && event->button == 1) { self->_graph->on_click_label(self->_collector_index); } + else if (event->type == GDK_BUTTON_PRESS && event->button == 3) { + self->_graph->on_popup_label(self->_collector_index); + } return TRUE; } @@ -262,8 +264,8 @@ button_press_event_callback(GtkWidget *widget, GdkEventButton *event, */ gboolean GtkStatsLabel:: query_tooltip_callback(GtkWidget *widget, gint x, gint y, - gboolean keyboard_tip, GtkTooltip *tooltip, - gpointer data) { + gboolean keyboard_tip, GtkTooltip *tooltip, + gpointer data) { GtkStatsLabel *self = (GtkStatsLabel *)data; std::string text = self->_graph->get_label_tooltip(self->_collector_index); diff --git a/pandatool/src/gtk-stats/gtkStatsMonitor.I b/pandatool/src/gtk-stats/gtkStatsMonitor.I index 65a5bfbf64..b3e8e55b13 100644 --- a/pandatool/src/gtk-stats/gtkStatsMonitor.I +++ b/pandatool/src/gtk-stats/gtkStatsMonitor.I @@ -14,10 +14,11 @@ /** * */ -GtkStatsMonitor::MenuDef:: -MenuDef(int thread_index, int collector_index, bool show_level) : +INLINE GtkStatsMonitor::MenuDef:: +MenuDef(int thread_index, int collector_index, ChartType chart_type, bool show_level) : _thread_index(thread_index), _collector_index(collector_index), + _chart_type(chart_type), _show_level(show_level), _monitor(nullptr) { @@ -26,7 +27,7 @@ MenuDef(int thread_index, int collector_index, bool show_level) : /** * */ -bool GtkStatsMonitor::MenuDef:: +INLINE bool GtkStatsMonitor::MenuDef:: operator < (const MenuDef &other) const { if (_thread_index != other._thread_index) { return _thread_index < other._thread_index; @@ -34,5 +35,8 @@ operator < (const MenuDef &other) const { if (_collector_index != other._collector_index) { return _collector_index < other._collector_index; } + if (_chart_type != other._chart_type) { + return _chart_type < other._chart_type; + } return (int)_show_level < (int)other._show_level; } diff --git a/pandatool/src/gtk-stats/gtkStatsMonitor.cxx b/pandatool/src/gtk-stats/gtkStatsMonitor.cxx index c215397339..ff41ed3a3f 100644 --- a/pandatool/src/gtk-stats/gtkStatsMonitor.cxx +++ b/pandatool/src/gtk-stats/gtkStatsMonitor.cxx @@ -18,10 +18,10 @@ #include "gtkStatsChartMenu.h" #include "gtkStatsPianoRoll.h" #include "gtkStatsFlameGraph.h" +#include "gtkStatsTimeline.h" #include "gtkStatsMenuId.h" #include "pStatGraph.h" #include "pStatCollectorDef.h" -#include "indent.h" /** * @@ -34,6 +34,8 @@ GtkStatsMonitor(GtkStatsServer *server) : PStatMonitor(server) { _time_units = 0; _scroll_speed = 0.0; _pause = false; + + _resolution = gdk_screen_get_resolution(gdk_screen_get_default()); } /** @@ -154,11 +156,13 @@ new_thread(int thread_index) { */ void GtkStatsMonitor:: new_data(int thread_index, int frame_number) { - Graphs::iterator gi; - for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) { - GtkStatsGraph *graph = (*gi); + for (GtkStatsGraph *graph : _graphs) { graph->new_data(thread_index, frame_number); } + + if (thread_index == 0) { + update_status_bar(); + } } /** @@ -193,6 +197,10 @@ idle() { sprintf(buffer, "%0.1f ms / %0.1f Hz", 1000.0f / frame_rate, frame_rate); gtk_label_set_text(GTK_LABEL(_frame_rate_label), buffer); + + if (!_status_bar_labels.empty()) { + gtk_label_set_text(GTK_LABEL(_status_bar_labels[0]), buffer); + } } } @@ -225,6 +233,14 @@ get_window() const { return _window; } +/** + * Returns the screen DPI. + */ +double GtkStatsMonitor:: +get_resolution() const { + return _resolution; +} + /** * Opens a new strip chart showing the indicated data. */ @@ -256,8 +272,21 @@ open_piano_roll(int thread_index) { * Opens a new flame graph showing the indicated data. */ void GtkStatsMonitor:: -open_flame_graph(int thread_index) { - GtkStatsFlameGraph *graph = new GtkStatsFlameGraph(this, thread_index); +open_flame_graph(int thread_index, int collector_index) { + GtkStatsFlameGraph *graph = new GtkStatsFlameGraph(this, thread_index, collector_index); + add_graph(graph); + + graph->set_time_units(_time_units); + graph->set_scroll_speed(_scroll_speed); + graph->set_pause(_pause); +} + +/** + * Opens a new timeline. + */ +void GtkStatsMonitor:: +open_timeline() { + GtkStatsTimeline *graph = new GtkStatsTimeline(this); add_graph(graph); graph->set_time_units(_time_units); @@ -371,7 +400,7 @@ create_window() { gtk_window_set_default_size(GTK_WINDOW(_window), 500, 360); // Set up the menu. - GtkAccelGroup *accel_group = gtk_accel_group_new(); + GtkAccelGroup *accel_group = gtk_accel_group_new(); gtk_window_add_accel_group(GTK_WINDOW(_window), accel_group); _menu_bar = gtk_menu_bar_new(); _next_chart_index = 2; @@ -390,8 +419,21 @@ create_window() { gtk_container_add(GTK_CONTAINER(_window), main_vbox); gtk_box_pack_start(GTK_BOX(main_vbox), _menu_bar, FALSE, TRUE, 0); + // Create the status bar. + _status_bar = gtk_flow_box_new(); + gtk_flow_box_set_activate_on_single_click(GTK_FLOW_BOX(_status_bar), FALSE); + gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(_status_bar), GTK_SELECTION_NONE); + g_signal_connect(G_OBJECT(_status_bar), "button_press_event", + G_CALLBACK(status_bar_button_event), this); + gtk_box_pack_end(GTK_BOX(main_vbox), _status_bar, FALSE, FALSE, 0); + update_status_bar(); + + GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL); + gtk_box_pack_end(GTK_BOX(main_vbox), sep, FALSE, FALSE, 0); + gtk_widget_show_all(_window); gtk_widget_show(_window); + gtk_widget_realize(_window); } /** @@ -572,6 +614,7 @@ setup_frame_rate_label() { _frame_rate_menu_item = gtk_menu_item_new(); _frame_rate_label = gtk_label_new(""); gtk_container_add(GTK_CONTAINER(_frame_rate_menu_item), _frame_rate_label); + gtk_widget_set_sensitive(_frame_rate_menu_item, FALSE); gtk_widget_show(_frame_rate_menu_item); gtk_widget_show(_frame_rate_label); @@ -579,3 +622,180 @@ setup_frame_rate_label() { gtk_menu_shell_append(GTK_MENU_SHELL(_menu_bar), _frame_rate_menu_item); } + +/** + * Updates the status bar. + */ +void GtkStatsMonitor:: +update_status_bar() { + const PStatClientData *client_data = get_client_data(); + if (client_data == nullptr) { + return; + } + + const PStatThreadData *thread_data = get_client_data()->get_thread_data(0); + if (thread_data == nullptr || thread_data->is_empty()) { + return; + } + const PStatFrameData &frame_data = thread_data->get_latest_frame(); + + pvector collectors; + + // The first label displays the frame rate. + size_t li = 1; + collectors.push_back(0); + if (_status_bar_labels.empty()) { + GtkWidget *label = gtk_label_new(""); + gtk_container_add(GTK_CONTAINER(_status_bar), label); + _status_bar_labels.push_back(label); + } + + // Gather the top-level collector list. + int num_toplevel_collectors = client_data->get_num_toplevel_collectors(); + for (int tc = 0; tc < num_toplevel_collectors; tc++) { + int collector = client_data->get_toplevel_collector(tc); + if (client_data->has_collector(collector) && + client_data->get_collector_has_level(collector, 0)) { + PStatView &view = get_level_view(collector, 0); + view.set_to_frame(frame_data); + double value = view.get_net_value(); + if (value == 0.0) { + // Don't include it unless we've included it before. + if (std::find(_status_bar_collectors.begin(), _status_bar_collectors.end(), collector) == _status_bar_collectors.end()) { + continue; + } + } + + const PStatCollectorDef &def = client_data->get_collector_def(collector); + std::string text = def._name; + text += ": " + PStatGraph::format_number(value, PStatGraph::GBU_named | PStatGraph::GBU_show_units, def._level_units); + + GtkWidget *label; + if (li < _status_bar_labels.size()) { + label = _status_bar_labels[li++]; + gtk_label_set_text(GTK_LABEL(label), text.c_str()); + } + else { + label = gtk_label_new(text.c_str()); + gtk_container_add(GTK_CONTAINER(_status_bar), label); + _status_bar_labels.push_back(label); + } + + collectors.push_back(collector); + } + } + + _status_bar_collectors = std::move(collectors); + + gtk_widget_show_all(_status_bar); +} + +/** + * Handles clicks on a partion of the status bar. + */ +gboolean GtkStatsMonitor:: +status_bar_button_event(GtkWidget *widget, GdkEventButton *event, gpointer data) { + GtkStatsMonitor *monitor = (GtkStatsMonitor *)data; + + GtkFlowBoxChild *child = gtk_flow_box_get_child_at_pos( + GTK_FLOW_BOX(monitor->_status_bar), event->x, event->y); + if (child == nullptr) { + return FALSE; + } + + // Which child is this? + GList *children = gtk_container_get_children(GTK_CONTAINER(monitor->_status_bar)); + int index = g_list_index(children, child); + g_list_free(children); + if (index < 0 || index >= monitor->_status_bar_labels.size()) { + return FALSE; + } + + const PStatClientData *client_data = monitor->get_client_data(); + if (client_data == nullptr) { + return FALSE; + } + + int collector = monitor->_status_bar_collectors[index]; + + if (event->type == GDK_2BUTTON_PRESS && event->button == 1) { + monitor->open_strip_chart(0, collector, collector != 0); + return TRUE; + } + else if (event->type == GDK_BUTTON_PRESS && event->button == 3 && index > 0) { + PStatView &level_view = monitor->get_level_view(collector, 0); + const PStatViewLevel *view_level = level_view.get_top_level(); + int num_children = view_level->get_num_children(); + if (num_children == 0) { + return FALSE; + } + + GtkWidget *menu = gtk_menu_new(); + + // Reverse the order since the menus are listed from the top down; we want + // to be visually consistent with the graphs, which list these labels from + // the bottom up. + for (int c = num_children - 1; c >= 0; c--) { + const PStatViewLevel *child_level = view_level->get_child(c); + + int child_collector = child_level->get_collector(); + const MenuDef *menu_def = monitor->add_menu({0, child_collector, CT_strip_chart, true}); + + double value = child_level->get_net_value(); + + const PStatCollectorDef &def = client_data->get_collector_def(child_collector); + std::string text = def._name; + text += ": " + PStatGraph::format_number(value, PStatGraph::GBU_named | PStatGraph::GBU_show_units, def._level_units); + + GtkWidget *menu_item = gtk_menu_item_new_with_label(text.c_str()); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(menu_activate), + (void *)menu_def); + } + + gtk_widget_show_all(menu); + + GtkWidget *label = monitor->_status_bar_labels[index]; + gtk_menu_popup_at_widget(GTK_MENU(menu), label, + GDK_GRAVITY_NORTH_WEST, + GDK_GRAVITY_SOUTH_WEST, nullptr); + return TRUE; + } + return FALSE; +} + +/** + * Callback when a menu item is selected. + */ +void GtkStatsMonitor:: +menu_activate(GtkWidget *widget, gpointer data) { + const MenuDef *menu_def = (const MenuDef *)data; + GtkStatsMonitor *monitor = menu_def->_monitor; + + if (monitor == nullptr) { + return; + } + + switch (menu_def->_chart_type) { + case CT_timeline: + monitor->open_timeline(); + break; + + case CT_strip_chart: + monitor->open_strip_chart(menu_def->_thread_index, + menu_def->_collector_index, + menu_def->_show_level); + break; + + case CT_flame_graph: + monitor->open_flame_graph(menu_def->_thread_index, + menu_def->_collector_index); + break; + + case CT_piano_roll: + monitor->open_piano_roll(menu_def->_thread_index); + break; + } +} diff --git a/pandatool/src/gtk-stats/gtkStatsMonitor.h b/pandatool/src/gtk-stats/gtkStatsMonitor.h index a79a6ad0ed..99f1baecee 100644 --- a/pandatool/src/gtk-stats/gtkStatsMonitor.h +++ b/pandatool/src/gtk-stats/gtkStatsMonitor.h @@ -34,13 +34,22 @@ class GtkStatsChartMenu; */ class GtkStatsMonitor : public PStatMonitor { public: + enum ChartType { + CT_timeline, + CT_strip_chart, + CT_flame_graph, + CT_piano_roll, + }; + class MenuDef { public: - INLINE MenuDef(int thread_index, int collector_index, bool show_level); + INLINE MenuDef(int thread_index, int collector_index, + ChartType chart_type, bool show_level = false); INLINE bool operator < (const MenuDef &other) const; int _thread_index; int _collector_index; + ChartType _chart_type; bool _show_level; GtkStatsMonitor *_monitor; }; @@ -64,9 +73,12 @@ public: virtual void user_guide_bars_changed(); GtkWidget *get_window() const; + double get_resolution() const; + void open_strip_chart(int thread_index, int collector_index, bool show_level); void open_piano_roll(int thread_index); - void open_flame_graph(int thread_index); + void open_flame_graph(int thread_index, int collector_index = -1); + void open_timeline(); const MenuDef *add_menu(const MenuDef &menu_def); @@ -86,7 +98,16 @@ private: void setup_options_menu(); void setup_speed_menu(); void setup_frame_rate_label(); + void update_status_bar(); + bool show_popup_menu(int collector); + static gboolean status_bar_button_event(GtkWidget *widget, + GdkEventButton *event, + gpointer data); +public: + static void menu_activate(GtkWidget *widget, gpointer data); + +private: typedef pset Graphs; Graphs _graphs; @@ -103,10 +124,14 @@ private: int _next_chart_index; GtkWidget *_frame_rate_menu_item; GtkWidget *_frame_rate_label; + GtkWidget *_status_bar; + pvector _status_bar_collectors; + pvector _status_bar_labels; std::string _window_title; int _time_units; double _scroll_speed; bool _pause; + double _resolution; friend class GtkStatsGraph; }; diff --git a/pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx b/pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx index 3f0bc320fa..4c86269f02 100644 --- a/pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx +++ b/pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx @@ -16,18 +16,16 @@ #include "numeric_types.h" #include "gtkStatsLabelStack.h" -static const int default_piano_roll_width = 600; -static const int default_piano_roll_height = 200; +static const int default_piano_roll_width = 800; +static const int default_piano_roll_height = 400; /** * */ GtkStatsPianoRoll:: GtkStatsPianoRoll(GtkStatsMonitor *monitor, int thread_index) : - PStatPianoRoll(monitor, thread_index, - default_piano_roll_width, - default_piano_roll_height), - GtkStatsGraph(monitor) + PStatPianoRoll(monitor, thread_index, 0, 0), + GtkStatsGraph(monitor, true) { // Let's show the units on the guide bar labels. There's room. set_guide_bar_units(get_guide_bar_units() | GBU_show_units); @@ -36,21 +34,21 @@ GtkStatsPianoRoll(GtkStatsMonitor *monitor, int thread_index) : // units. _scale_area = gtk_drawing_area_new(); g_signal_connect(G_OBJECT(_scale_area), "draw", - G_CALLBACK(draw_callback), this); - gtk_box_pack_start(GTK_BOX(_graph_vbox), _scale_area, - FALSE, FALSE, 0); + G_CALLBACK(draw_callback), this); + gtk_box_pack_start(GTK_BOX(_graph_vbox), _scale_area, FALSE, FALSE, 0); // It should be large enough to display the labels. { - PangoLayout *layout = gtk_widget_create_pango_layout(_window, "0123456789 ms"); + PangoLayout *layout = gtk_widget_create_pango_layout(_scale_area, "0123456789 ms"); int width, height; pango_layout_get_pixel_size(layout, &width, &height); gtk_widget_set_size_request(_scale_area, 0, height + 1); g_object_unref(layout); } - gtk_widget_set_size_request(_graph_window, default_piano_roll_width, - default_piano_roll_height); + gtk_widget_set_size_request(_graph_window, + default_piano_roll_width * monitor->get_resolution() / 96, + default_piano_roll_height * monitor->get_resolution() / 96); const PStatClientData *client_data = GtkStatsGraph::_monitor->get_client_data(); @@ -77,7 +75,7 @@ GtkStatsPianoRoll:: } /** - * Called as each frame's data is made available. There is no gurantee the + * Called as each frame's data is made available. There is no guarantee the * frames will arrive in order, or that all of them will arrive at all. The * monitor should be prepared to accept frames received out-of-order or * missing. @@ -94,7 +92,9 @@ new_data(int thread_index, int frame_number) { */ void GtkStatsPianoRoll:: force_redraw() { - PStatPianoRoll::force_redraw(); + if (_cr) { + PStatPianoRoll::force_redraw(); + } } /** @@ -132,6 +132,57 @@ on_click_label(int collector_index) { } } +/** + * Called when the user right-clicks on a label. + */ +void GtkStatsPianoRoll:: +on_popup_label(int collector_index) { + GtkWidget *menu = gtk_menu_new(); + + std::string label = get_label_tooltip(collector_index); + if (!label.empty()) { + GtkWidget *menu_item = gtk_menu_item_new_with_label(label.c_str()); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + gtk_widget_set_sensitive(menu_item, FALSE); + } + + { + const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({ + _thread_index, collector_index, GtkStatsMonitor::CT_strip_chart, false, + }); + + GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Strip Chart"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(GtkStatsMonitor::menu_activate), + (void *)menu_def); + } + + { + const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({ + _thread_index, collector_index, GtkStatsMonitor::CT_flame_graph, + }); + + GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Flame Graph"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(GtkStatsMonitor::menu_activate), + (void *)menu_def); + } + + gtk_widget_show_all(menu); + gtk_menu_popup_at_pointer(GTK_MENU(menu), nullptr); +} + +/** + * Called when the mouse hovers over a label, and should return the text that + * should appear on the tooltip. + */ +std::string GtkStatsPianoRoll:: +get_label_tooltip(int collector_index) const { + return PStatPianoRoll::get_label_tooltip(collector_index); +} + /** * Changes the amount of time the width of the horizontal axis represents. * This may force a redraw. @@ -188,7 +239,7 @@ draw_bar(int row, int from_x, int to_x) { int y = _label_stack.get_label_y(row, _graph_window); int height = _label_stack.get_label_height(row); - cairo_rectangle(_cr, from_x, y - height + 2, to_x - from_x, height - 4); + cairo_rectangle(_cr, from_x, (y - height + 2) * _cr_scale, to_x - from_x, (height - 4) * _cr_scale); cairo_fill(_cr); } } @@ -224,6 +275,19 @@ additional_graph_window_paint(cairo_t *cr) { } } +/** + * Called when the mouse hovers over the graph, and should return the text that + * should appear on the tooltip. + */ +std::string GtkStatsPianoRoll:: +get_graph_tooltip(int mouse_x, int mouse_y) const { + int collector_index = get_collector_under_pixel(mouse_x, mouse_y); + if (collector_index >= 0) { + return get_label_tooltip(collector_index); + } + return std::string(); +} + /** * Based on the mouse position within the graph window, look for draggable * things the mouse might be hovering over and return the appropriate DragMode @@ -256,13 +320,24 @@ consider_drag_start(int graph_x, int graph_y) { * Called when the mouse button is depressed within the graph window. */ gboolean GtkStatsPianoRoll:: -handle_button_press(GtkWidget *widget, int graph_x, int graph_y, - bool double_click) { - if (double_click) { - // Double-clicking on a color bar in the graph is the same as double- - // clicking on the corresponding label. - on_click_label(get_collector_under_pixel(graph_x, graph_y)); - return TRUE; +handle_button_press(int graph_x, int graph_y, bool double_click, int button) { + if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) { + int collector_index = get_collector_under_pixel(graph_x, graph_y); + if (button == 3) { + // Right-clicking on a color bar in the graph is the same as right- + // clicking on the corresponding label. + if (collector_index >= 0) { + on_popup_label(collector_index); + return TRUE; + } + return FALSE; + } + else if (double_click && button == 1) { + // Double-clicking on a color bar in the graph is the same as double- + // clicking on the corresponding label. + on_click_label(get_collector_under_pixel(graph_x, graph_y)); + return TRUE; + } } if (_potential_drag_mode == DM_none) { @@ -278,21 +353,21 @@ handle_button_press(GtkWidget *widget, int graph_x, int graph_y, return TRUE; } - return GtkStatsGraph::handle_button_press(widget, graph_x, graph_y, - double_click); + return GtkStatsGraph::handle_button_press(graph_x, graph_y, + double_click, button); } /** * Called when the mouse button is released within the graph window. */ gboolean GtkStatsPianoRoll:: -handle_button_release(GtkWidget *widget, int graph_x, int graph_y) { +handle_button_release(int graph_x, int graph_y) { if (_drag_mode == DM_scale) { set_drag_mode(DM_none); // ReleaseCapture(); - return handle_motion(widget, graph_x, graph_y); - - } else if (_drag_mode == DM_guide_bar) { + return handle_motion(graph_x, graph_y); + } + else if (_drag_mode == DM_guide_bar) { if (graph_x < 0 || graph_x >= get_xsize()) { remove_user_guide_bar(_drag_guide_bar); } else { @@ -300,17 +375,17 @@ handle_button_release(GtkWidget *widget, int graph_x, int graph_y) { } set_drag_mode(DM_none); // ReleaseCapture(); - return handle_motion(widget, graph_x, graph_y); + return handle_motion(graph_x, graph_y); } - return GtkStatsGraph::handle_button_release(widget, graph_x, graph_y); + return GtkStatsGraph::handle_button_release(graph_x, graph_y); } /** * Called when the mouse is moved within the graph window. */ gboolean GtkStatsPianoRoll:: -handle_motion(GtkWidget *widget, int graph_x, int graph_y) { +handle_motion(int graph_x, int graph_y) { if (_drag_mode == DM_none && _potential_drag_mode == DM_none) { // When the mouse is over a color bar, highlight it. int collector_index = get_collector_under_pixel(graph_x, graph_y); @@ -328,8 +403,8 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) { }; TrackMouseEvent(&tme); */ - - } else { + } + else { // If the mouse is in some drag mode, stop highlighting. _label_stack.highlight_label(-1); on_leave_label(_highlighted_index); @@ -341,8 +416,8 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) { set_horizontal_scale(_drag_scale_start / ratio); } return TRUE; - - } else if (_drag_mode == DM_new_guide_bar) { + } + else if (_drag_mode == DM_new_guide_bar) { // We haven't created the new guide bar yet; we won't until the mouse // comes within the graph's region. if (graph_x >= 0 && graph_x < get_xsize()) { @@ -356,7 +431,17 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) { return TRUE; } - return GtkStatsGraph::handle_motion(widget, graph_x, graph_y); + return GtkStatsGraph::handle_motion(graph_x, graph_y); +} + +/** + * Called when the mouse has left the graph window. + */ +gboolean GtkStatsPianoRoll:: +handle_leave() { + _label_stack.highlight_label(-1); + on_leave_label(_highlighted_index); + return TRUE; } /** @@ -364,14 +449,14 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) { * -1. */ int GtkStatsPianoRoll:: -get_collector_under_pixel(int xpoint, int ypoint) { +get_collector_under_pixel(int xpoint, int ypoint) const { if (_label_stack.get_num_labels() == 0) { return -1; } // Assume all of the labels are the same height. int height = _label_stack.get_label_height(0); - int row = (get_ysize() - ypoint) / height; + int row = (get_ysize() - ypoint) / (height * _cr_scale); if (row >= 0 && row < _label_stack.get_num_labels()) { return _label_stack.get_label_collector_index(row); } else { @@ -411,7 +496,7 @@ draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar) { cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]); break; - case GBS_normal: + default: cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]); break; } @@ -452,7 +537,7 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) { cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]); break; - case GBS_normal: + default: cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]); break; } @@ -460,13 +545,13 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) { int x = height_to_pixel(bar._height); const std::string &label = bar._label; - PangoLayout *layout = gtk_widget_create_pango_layout(_window, label.c_str()); + PangoLayout *layout = gtk_widget_create_pango_layout(_scale_area, label.c_str()); int width, height; pango_layout_get_pixel_size(layout, &width, &height); if (bar._style != GBS_user) { - double from_height = pixel_to_height(x - width); - double to_height = pixel_to_height(x + width); + double from_height = pixel_to_height(x - width * _cr_scale); + double to_height = pixel_to_height(x + width * _cr_scale); if (find_user_guide_bar(from_height, to_height) >= 0) { // Omit the label: there's a user-defined guide bar in the same space. g_object_unref(layout); @@ -478,6 +563,8 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) { // Now convert our x to a coordinate within our drawing area. int junk_y; + x /= _cr_scale; + // The x coordinate comes from the graph_window. gtk_widget_translate_coordinates(_graph_window, _scale_area, x, 0, diff --git a/pandatool/src/gtk-stats/gtkStatsPianoRoll.h b/pandatool/src/gtk-stats/gtkStatsPianoRoll.h index a7cfd6afe8..18a30ee633 100644 --- a/pandatool/src/gtk-stats/gtkStatsPianoRoll.h +++ b/pandatool/src/gtk-stats/gtkStatsPianoRoll.h @@ -39,6 +39,8 @@ public: virtual void set_time_units(int unit_mask); virtual void on_click_label(int collector_index); + virtual void on_popup_label(int collector_index); + virtual std::string get_label_tooltip(int collector_index) const; void set_horizontal_scale(double time_width); protected: @@ -50,15 +52,17 @@ protected: virtual void idle(); virtual void additional_graph_window_paint(cairo_t *cr); + virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const; virtual DragMode consider_drag_start(int graph_x, int graph_y); - virtual gboolean handle_button_press(GtkWidget *widget, int graph_x, int graph_y, - bool double_click); - virtual gboolean handle_button_release(GtkWidget *widget, int graph_x, int graph_y); - virtual gboolean handle_motion(GtkWidget *widget, int graph_x, int graph_y); + virtual gboolean handle_button_press(int graph_x, int graph_y, + bool double_click, int button); + virtual gboolean handle_button_release(int graph_x, int graph_y); + virtual gboolean handle_motion(int graph_x, int graph_y); + virtual gboolean handle_leave(); private: - int get_collector_under_pixel(int xpoint, int ypoint); + int get_collector_under_pixel(int xpoint, int ypoint) const; void update_labels(); void draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar); void draw_guide_labels(cairo_t *cr); diff --git a/pandatool/src/gtk-stats/gtkStatsStripChart.cxx b/pandatool/src/gtk-stats/gtkStatsStripChart.cxx index eb39d875a9..78c31d6e45 100644 --- a/pandatool/src/gtk-stats/gtkStatsStripChart.cxx +++ b/pandatool/src/gtk-stats/gtkStatsStripChart.cxx @@ -26,12 +26,9 @@ GtkStatsStripChart:: GtkStatsStripChart(GtkStatsMonitor *monitor, int thread_index, int collector_index, bool show_level) : PStatStripChart(monitor, - show_level ? monitor->get_level_view(collector_index, thread_index) : monitor->get_view(thread_index), - thread_index, - collector_index, - default_strip_chart_width, - default_strip_chart_height), - GtkStatsGraph(monitor) + show_level ? monitor->get_level_view(0, thread_index) : monitor->get_view(thread_index), + thread_index, collector_index, 0, 0), + GtkStatsGraph(monitor, true) { if (show_level) { // If it's a level-type graph, show the appropriate units. @@ -48,38 +45,35 @@ GtkStatsStripChart(GtkStatsMonitor *monitor, int thread_index, // Put some stuff on top of the graph. _top_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); - gtk_box_pack_start(GTK_BOX(_graph_vbox), _top_hbox, - FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(_graph_vbox), _top_hbox, FALSE, FALSE, 0); _smooth_check_box = gtk_check_button_new_with_label("Smooth"); g_signal_connect(G_OBJECT(_smooth_check_box), "toggled", - G_CALLBACK(toggled_callback), this); + G_CALLBACK(toggled_callback), this); _total_label = gtk_label_new(""); - gtk_box_pack_start(GTK_BOX(_top_hbox), _smooth_check_box, - FALSE, FALSE, 0); - gtk_box_pack_end(GTK_BOX(_top_hbox), _total_label, - FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(_top_hbox), _smooth_check_box, FALSE, FALSE, 0); + gtk_box_pack_end(GTK_BOX(_top_hbox), _total_label, FALSE, FALSE, 0); // Add a DrawingArea widget to the right of the graph, to display all of the // scale units. _scale_area = gtk_drawing_area_new(); g_signal_connect(G_OBJECT(_scale_area), "draw", - G_CALLBACK(draw_callback), this); - gtk_box_pack_start(GTK_BOX(_graph_hbox), _scale_area, - FALSE, FALSE, 0); + G_CALLBACK(draw_callback), this); + gtk_box_pack_start(GTK_BOX(_graph_hbox), _scale_area, FALSE, FALSE, 0); // Make it wide enough to display a typical label. { - PangoLayout *layout = gtk_widget_create_pango_layout(_window, "99 ms"); + PangoLayout *layout = gtk_widget_create_pango_layout(_scale_area, "99 ms"); int width, height; pango_layout_get_pixel_size(layout, &width, &height); gtk_widget_set_size_request(_scale_area, width, 0); g_object_unref(layout); } - gtk_widget_set_size_request(_graph_window, default_strip_chart_width, - default_strip_chart_height); + gtk_widget_set_size_request(_graph_window, + default_strip_chart_width * monitor->get_resolution() / 96, + default_strip_chart_height * monitor->get_resolution() / 96); gtk_widget_show_all(_window); gtk_widget_show(_window); @@ -87,7 +81,7 @@ GtkStatsStripChart(GtkStatsMonitor *monitor, int thread_index, // Allow the window to be resized as small as the user likes. We have to do // this after the window has been shown; otherwise, it will affect the // window's initial size. - gtk_widget_set_size_request(_window, 0, 0); + gtk_widget_set_size_request(_graph_window, 0, 0); clear_region(); } @@ -108,7 +102,7 @@ new_collector(int collector_index) { } /** - * Called as each frame's data is made available. There is no gurantee the + * Called as each frame's data is made available. There is no guarantee the * frames will arrive in order, or that all of them will arrive at all. The * monitor should be prepared to accept frames received out-of-order or * missing. @@ -138,7 +132,9 @@ new_data(int thread_index, int frame_number) { */ void GtkStatsStripChart:: force_redraw() { - PStatStripChart::force_redraw(); + if (_cr) { + PStatStripChart::force_redraw(); + } } /** @@ -209,6 +205,66 @@ on_click_label(int collector_index) { } } +/** + * Called when the user right-clicks on a label. + */ +void GtkStatsStripChart:: +on_popup_label(int collector_index) { + GtkWidget *menu = gtk_menu_new(); + _popup_index = collector_index; + + std::string label = get_label_tooltip(collector_index); + if (!label.empty()) { + GtkWidget *menu_item = gtk_menu_item_new_with_label(label.c_str()); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + gtk_widget_set_sensitive(menu_item, FALSE); + } + + { + GtkWidget *menu_item = gtk_menu_item_new_with_label("Set as Focus"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + + if (collector_index == 0 && get_collector_index() == 0) { + gtk_widget_set_sensitive(menu_item, FALSE); + } else { + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(+[] (GtkWidget *widget, gpointer data) { + GtkStatsStripChart *self = (GtkStatsStripChart *)data; + self->set_collector_index(self->_popup_index); + }), + this); + } + } + + { + const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({ + _thread_index, collector_index, + GtkStatsMonitor::CT_strip_chart, get_view().get_show_level(), + }); + + GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Strip Chart"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(GtkStatsMonitor::menu_activate), + (void *)menu_def); + } + + if (!get_view().get_show_level()) { + const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({ + _thread_index, collector_index, GtkStatsMonitor::CT_flame_graph, + }); + + GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Flame Graph"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(GtkStatsMonitor::menu_activate), + (void *)menu_def); + } + + gtk_widget_show_all(menu); + gtk_menu_popup_at_pointer(GTK_MENU(menu), nullptr); +} + /** * Called when the mouse hovers over a label, and should return the text that * should appear on the tooltip. @@ -263,8 +319,9 @@ copy_region(int start_x, int end_x, int dest_x) { // We are not allowed to copy a surface onto itself, so we have to create a // temporary surface to copy to. end_x = std::min(end_x, get_xsize()); + GdkWindow *window = gtk_widget_get_window(_graph_window); cairo_surface_t *temp_surface = - cairo_image_surface_create(CAIRO_FORMAT_RGB24, end_x - start_x, get_ysize()); + gdk_window_create_similar_image_surface(window, CAIRO_FORMAT_RGB24, end_x - start_x, get_ysize(), 1); { cairo_t *temp_cr = cairo_create(temp_surface); cairo_set_source_surface(temp_cr, _cr_surface, -start_x, 0); @@ -278,11 +335,7 @@ copy_region(int start_x, int end_x, int dest_x) { cairo_surface_destroy(temp_surface); - GdkWindow *window = gtk_widget_get_window(_graph_window); - GdkRectangle rect = { - dest_x, 0, end_x - start_x, get_ysize() - }; - gdk_window_invalidate_rect(window, &rect, FALSE); + gdk_window_invalidate_rect(window, nullptr, FALSE); } /** @@ -356,8 +409,9 @@ end_draw(int from_x, int to_x) { } GdkWindow *window = gtk_widget_get_window(_graph_window); + int scale = gdk_window_get_scale_factor(window); GdkRectangle rect = { - from_x, 0, to_x - from_x, get_ysize() + (from_x * scale) / scale, 0, (to_x - from_x) / scale, get_ysize() / scale }; gdk_window_invalidate_rect(window, &rect, FALSE); } @@ -374,6 +428,18 @@ additional_graph_window_paint(cairo_t *cr) { } } +/** + * Called when the mouse hovers over the graph, and should return the text that + * should appear on the tooltip. + */ +std::string GtkStatsStripChart:: +get_graph_tooltip(int mouse_x, int mouse_y) const { + if (_highlighted_index != -1) { + return get_label_tooltip(_highlighted_index); + } + return std::string(); +} + /** * Based on the mouse position within the graph window, look for draggable * things the mouse might be hovering over and return the appropriate DragMode @@ -409,19 +475,11 @@ void GtkStatsStripChart:: set_drag_mode(GtkStatsGraph::DragMode drag_mode) { GtkStatsGraph::set_drag_mode(drag_mode); - switch (_drag_mode) { - case DM_scale: - case DM_sizing: - // Disable smoothing for these expensive operations. - set_average_mode(false); - break; - - default: + if (_drag_mode == DM_none) { // Restore smoothing according to the current setting of the check box. bool active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(_smooth_check_box)); set_average_mode(active); - break; } } @@ -429,10 +487,19 @@ set_drag_mode(GtkStatsGraph::DragMode drag_mode) { * Called when the mouse button is depressed within the graph window. */ gboolean GtkStatsStripChart:: -handle_button_press(GtkWidget *widget, int graph_x, int graph_y, - bool double_click) { +handle_button_press(int graph_x, int graph_y, bool double_click, int button) { if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) { - if (double_click) { + int collector_index = get_collector_under_pixel(graph_x, graph_y); + if (button == 3) { + // Right-clicking on a color bar in the graph is the same as right- + // clicking on the corresponding label. + if (collector_index >= 0) { + on_popup_label(collector_index); + return TRUE; + } + return FALSE; + } + else if (double_click && button == 1) { // Double-clicking on a color bar in the graph is the same as double- // clicking on the corresponding label. on_click_label(get_collector_under_pixel(graph_x, graph_y)); @@ -454,21 +521,21 @@ handle_button_press(GtkWidget *widget, int graph_x, int graph_y, return TRUE; } - return GtkStatsGraph::handle_button_press(widget, graph_x, graph_y, - double_click); + return GtkStatsGraph::handle_button_press(graph_x, graph_y, + double_click, button); } /** * Called when the mouse button is released within the graph window. */ gboolean GtkStatsStripChart:: -handle_button_release(GtkWidget *widget, int graph_x, int graph_y) { +handle_button_release(int graph_x, int graph_y) { if (_drag_mode == DM_scale) { set_drag_mode(DM_none); // ReleaseCapture(); - return handle_motion(widget, graph_x, graph_y); - - } else if (_drag_mode == DM_guide_bar) { + return handle_motion(graph_x, graph_y); + } + else if (_drag_mode == DM_guide_bar) { if (graph_y < 0 || graph_y >= get_ysize()) { remove_user_guide_bar(_drag_guide_bar); } else { @@ -476,17 +543,17 @@ handle_button_release(GtkWidget *widget, int graph_x, int graph_y) { } set_drag_mode(DM_none); // ReleaseCapture(); - return handle_motion(widget, graph_x, graph_y); + return handle_motion(graph_x, graph_y); } - return GtkStatsGraph::handle_button_release(widget, graph_x, graph_y); + return GtkStatsGraph::handle_button_release(graph_x, graph_y); } /** * Called when the mouse is moved within the graph window. */ gboolean GtkStatsStripChart:: -handle_motion(GtkWidget *widget, int graph_x, int graph_y) { +handle_motion(int graph_x, int graph_y) { if (_drag_mode == DM_none && _potential_drag_mode == DM_none && graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) { // When the mouse is over a color bar, highlight it. @@ -501,13 +568,18 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) { } if (_drag_mode == DM_scale) { - double ratio = 1.0f - ((double)graph_y / (double)get_ysize()); - if (ratio > 0.0f) { - set_vertical_scale(_drag_scale_start / ratio); + double ratio = 1.0 - ((double)graph_y / (double)get_ysize()); + if (ratio > 0.0) { + double new_scale = _drag_scale_start / ratio; + if (!IS_NEARLY_EQUAL(get_vertical_scale(), new_scale)) { + // Disable smoothing while we do this expensive operation. + set_average_mode(false); + set_vertical_scale(_drag_scale_start / ratio); + } } return TRUE; - - } else if (_drag_mode == DM_new_guide_bar) { + } + else if (_drag_mode == DM_new_guide_bar) { // We haven't created the new guide bar yet; we won't until the mouse // comes within the graph's region. if (graph_y >= 0 && graph_y < get_ysize()) { @@ -515,13 +587,23 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) { _drag_guide_bar = add_user_guide_bar(pixel_to_height(graph_y)); return TRUE; } - - } else if (_drag_mode == DM_guide_bar) { + } + else if (_drag_mode == DM_guide_bar) { move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_y)); return TRUE; } - return GtkStatsGraph::handle_motion(widget, graph_x, graph_y); + return GtkStatsGraph::handle_motion(graph_x, graph_y); +} + +/** + * Called when the mouse has left the graph window. + */ +gboolean GtkStatsStripChart:: +handle_leave() { + _label_stack.highlight_label(-1); + on_leave_label(_highlighted_index); + return TRUE; } /** @@ -543,7 +625,7 @@ draw_guide_bar(cairo_t *cr, int from_x, int to_x, cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]); break; - case GBS_normal: + default: cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]); break; } @@ -593,7 +675,7 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar, int last_y) { cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]); break; - case GBS_normal: + default: cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]); break; } @@ -601,13 +683,13 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar, int last_y) { int y = height_to_pixel(bar._height); const std::string &label = bar._label; - PangoLayout *layout = gtk_widget_create_pango_layout(_window, label.c_str()); + PangoLayout *layout = gtk_widget_create_pango_layout(_scale_area, label.c_str()); int width, height; pango_layout_get_pixel_size(layout, &width, &height); if (bar._style != GBS_user) { - double from_height = pixel_to_height(y + height); - double to_height = pixel_to_height(y - height); + double from_height = pixel_to_height(y + height * _cr_scale); + double to_height = pixel_to_height(y - height * _cr_scale); if (find_user_guide_bar(from_height, to_height) >= 0) { // Omit the label: there's a user-defined guide bar in the same space. g_object_unref(layout); @@ -619,6 +701,8 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar, int last_y) { // Now convert our y to a coordinate within our drawing area. int junk_x; + y /= _cr_scale; + // The y coordinate comes from the graph_window. gtk_widget_translate_coordinates(_graph_window, _scale_area, 0, y, diff --git a/pandatool/src/gtk-stats/gtkStatsStripChart.h b/pandatool/src/gtk-stats/gtkStatsStripChart.h index b1fc44b88e..01284e9719 100644 --- a/pandatool/src/gtk-stats/gtkStatsStripChart.h +++ b/pandatool/src/gtk-stats/gtkStatsStripChart.h @@ -41,6 +41,7 @@ public: virtual void set_time_units(int unit_mask); virtual void set_scroll_speed(double scroll_speed); virtual void on_click_label(int collector_index); + virtual void on_popup_label(int collector_index); virtual std::string get_label_tooltip(int collector_index) const; void set_vertical_scale(double value_height); @@ -56,13 +57,15 @@ protected: virtual void end_draw(int from_x, int to_x); virtual void additional_graph_window_paint(cairo_t *cr); + virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const; virtual DragMode consider_drag_start(int graph_x, int graph_y); virtual void set_drag_mode(DragMode drag_mode); - virtual gboolean handle_button_press(GtkWidget *widget, int graph_x, int graph_y, - bool double_click); - virtual gboolean handle_button_release(GtkWidget *widget, int graph_x, int graph_y); - virtual gboolean handle_motion(GtkWidget *widget, int graph_x, int graph_y); + virtual gboolean handle_button_press(int graph_x, int graph_y, + bool double_click, int button); + virtual gboolean handle_button_release(int graph_x, int graph_y); + virtual gboolean handle_motion(int graph_x, int graph_y); + virtual gboolean handle_leave(); private: void draw_guide_bar(cairo_t *cr, int from_x, int to_x, @@ -79,6 +82,8 @@ private: GtkWidget *_top_hbox; GtkWidget *_smooth_check_box; GtkWidget *_total_label; + + int _popup_index = -1; }; #endif diff --git a/pandatool/src/gtk-stats/gtkStatsTimeline.cxx b/pandatool/src/gtk-stats/gtkStatsTimeline.cxx new file mode 100644 index 0000000000..fbc45ccc1b --- /dev/null +++ b/pandatool/src/gtk-stats/gtkStatsTimeline.cxx @@ -0,0 +1,812 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file gtkStatsTimeline.cxx + * @author rdb + * @date 2022-02-17 + */ + +#include "gtkStatsTimeline.h" +#include "gtkStatsMonitor.h" +#include "numeric_types.h" +#include "gtkStatsLabelStack.h" + +static const int default_timeline_width = 1000; +static const int default_timeline_height = 300; + +/** + * + */ +GtkStatsTimeline:: +GtkStatsTimeline(GtkStatsMonitor *monitor) : + PStatTimeline(monitor, 0, 0), + GtkStatsGraph(monitor, false) +{ + // Let's show the units on the guide bar labels. There's room. + set_guide_bar_units(get_guide_bar_units() | GBU_show_units); + + // Add a DrawingArea widget on top of the graph, to display all of the scale + // units. + _scale_area = gtk_drawing_area_new(); + g_signal_connect(G_OBJECT(_scale_area), "draw", + G_CALLBACK(scale_area_draw_callback), this); + gtk_box_pack_start(GTK_BOX(_graph_vbox), _scale_area, FALSE, FALSE, 0); + + // It should be large enough to display the labels. + { + PangoLayout *layout = gtk_widget_create_pango_layout(_scale_area, "0123456789 ms"); + int width, height; + pango_layout_get_pixel_size(layout, &width, &height); + gtk_widget_set_size_request(_scale_area, 0, height + _pixel_scale / 2); + g_object_unref(layout); + } + + // Add a drawing area to the left of the graph to show the thread labels. + _thread_area = gtk_drawing_area_new(); + gtk_box_pack_start(GTK_BOX(_graph_hbox), _thread_area, FALSE, FALSE, 0); + gtk_box_reorder_child(GTK_BOX(_graph_hbox), _thread_area, 0); + g_signal_connect(G_OBJECT(_thread_area), "draw", + G_CALLBACK(thread_area_draw_callback), this); + + // Listen for mouse wheel and keyboard events. + gtk_widget_add_events(_graph_window, GDK_SCROLL_MASK | + GDK_KEY_PRESS_MASK | + GDK_KEY_RELEASE_MASK); + gtk_widget_set_can_focus(_graph_window, TRUE); + g_signal_connect(G_OBJECT(_graph_window), "scroll_event", + G_CALLBACK(scroll_callback), this); + g_signal_connect(G_OBJECT(_graph_window), "key_press_event", + G_CALLBACK(key_press_callback), this); + g_signal_connect(G_OBJECT(_graph_window), "key_release_event", + G_CALLBACK(key_release_callback), this); + + int min_height = 0; + if (!_threads.empty()) { + int num_rows = _threads.back()._row_offset + _threads.back()._rows.size(); + double height = row_to_pixel(num_rows) + _pixel_scale * 2.5; + min_height = height / _cr_scale; + } + + gtk_widget_set_size_request(_graph_window, + default_timeline_width * monitor->get_resolution() / 96, + std::max(min_height, (int)(default_timeline_height * monitor->get_resolution() / 96))); + + gtk_window_set_title(GTK_WINDOW(_window), "Timeline"); + + _grid_pattern = cairo_pattern_create_rgb(0xdd / 255.0, 0xdd / 255.0, 0xdd / 255.0); + + gtk_widget_show_all(_window); + gtk_widget_show(_window); + + // Allow the window to be resized as small as the user likes. We have to do + // this after the window has been shown; otherwise, it will affect the + // window's initial size. + gtk_widget_set_size_request(_graph_window, 0, min_height); + + clear_region(); +} + +/** + * + */ +GtkStatsTimeline:: +~GtkStatsTimeline() { + cairo_pattern_destroy(_grid_pattern); +} + +/** + * Called as each frame's data is made available. There is no guarantee the + * frames will arrive in order, or that all of them will arrive at all. The + * monitor should be prepared to accept frames received out-of-order or + * missing. + */ +void GtkStatsTimeline:: +new_data(int thread_index, int frame_number) { + PStatTimeline::new_data(thread_index, frame_number); +} + +/** + * Called when it is necessary to redraw the entire graph. + */ +void GtkStatsTimeline:: +force_redraw() { + assert(_cr); + if (_cr) { + PStatTimeline::force_redraw(); + } +} + +/** + * Called when the user has resized the window, forcing a resize of the graph. + */ +void GtkStatsTimeline:: +changed_graph_size(int graph_xsize, int graph_ysize) { + PStatTimeline::changed_size(graph_xsize, graph_ysize); +} + +/** + * Erases the chart area. + */ +void GtkStatsTimeline:: +clear_region() { + cairo_set_source_rgb(_cr, 1.0, 1.0, 1.0); + cairo_paint(_cr); +} + +/** + * Erases the chart area in preparation for drawing a bunch of bars. + */ +void GtkStatsTimeline:: +begin_draw() { +} + +/** + * Draws a horizontal separator. + */ +void GtkStatsTimeline:: +draw_separator(int row) { + cairo_set_source(_cr, _grid_pattern); + cairo_rectangle(_cr, 0, (row_to_pixel(row) + row_to_pixel(row + 1)) / 2.0, + get_xsize(), _pixel_scale * 1 / 3); + cairo_fill(_cr); +} + +/** + * Draws a vertical guide bar. If the row is -1, draws it in all rows. + */ +void GtkStatsTimeline:: +draw_guide_bar(int x, GuideBarStyle style) { + double width = _pixel_scale / 3.0; + if (style == GBS_frame) { + width *= 2; + } + + cairo_set_source(_cr, _grid_pattern); + cairo_rectangle(_cr, x - width / 2.0, 0, width, get_ysize()); + cairo_fill(_cr); +} + +/** + * Draws a single bar in the chart for the indicated row, in the color for the + * given collector, for the indicated horizontal pixel range. + */ +void GtkStatsTimeline:: +draw_bar(int row, int from_x, int to_x, int collector_index, + const std::string &collector_name) { + int top = row_to_pixel(row); + int bottom = row_to_pixel(row + 1); + int scale = _pixel_scale; + + bool is_highlighted = row == _highlighted_row && _highlighted_x >= from_x && _highlighted_x < to_x; + cairo_set_source(_cr, get_collector_pattern(collector_index, is_highlighted)); + + if (to_x < from_x + 1) { + // Too tiny to draw. + } + else if (to_x < from_x + scale) { + // It's just a tiny sliver. This is a more reliable way to draw it. + cairo_rectangle(_cr, from_x, top, to_x - from_x, bottom - top); + cairo_fill(_cr); + } + else { + int left = std::max(from_x, -scale - 1); + int right = std::min(std::max(to_x, from_x + 1), get_xsize() + scale); + + double radius = std::min((double)scale, (right - left) / 2.0); + cairo_new_sub_path(_cr); + cairo_arc(_cr, right - radius, top + radius, radius, -0.5 * M_PI, 0.0); + cairo_arc(_cr, right - radius, bottom - radius, radius, 0.0, 0.5 * M_PI); + cairo_arc(_cr, left + radius, bottom - radius, radius, 0.5 * M_PI, M_PI); + cairo_arc(_cr, left + radius, top + radius, radius, M_PI, 1.5 * M_PI); + cairo_close_path(_cr); + cairo_fill(_cr); + + if ((to_x - from_x) >= scale * 4) { + // Only bother drawing the text if we've got some space to draw on. + // Choose a suitable foreground color. + LRGBColor fg = get_collector_text_color(collector_index, is_highlighted); + cairo_set_source_rgb(_cr, fg[0], fg[1], fg[2]); + + // Make sure that the text doesn't run off the chart. + int text_width, text_height; + PangoLayout *layout = gtk_widget_create_pango_layout(_graph_window, collector_name.c_str()); + pango_layout_set_attributes(layout, _pango_attrs); + pango_layout_set_height(layout, -1); + pango_layout_get_pixel_size(layout, &text_width, &text_height); + + double center = (from_x + to_x) / 2.0; + double text_left = std::max(from_x, 0) + scale / 2.0; + double text_right = std::min(to_x, get_xsize()) - scale / 2.0; + double text_top = top + (bottom - top - text_height) / 2.0; + + if (text_width >= text_right - text_left) { + if (text_right - text_left < scale * 6) { + // It's a really tiny space. Draw a single letter. + pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER); + pango_layout_set_text(layout, collector_name.c_str(), 1); + } else { + // It's going to be tricky to fit it, let pango figure it out. + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); + } + pango_layout_set_width(layout, (text_right - text_left) * PANGO_SCALE); + cairo_move_to(_cr, text_left, text_top); + } + else if (center - text_width / 2.0 < 0.0) { + // Put it against the left-most edge. + cairo_move_to(_cr, scale, text_top); + } + else if (center + text_width / 2.0 >= get_xsize()) { + // Put it against the right-most edge. + cairo_move_to(_cr, get_xsize() - scale - text_width, text_top); + } + else { + // It fits just fine, center it. + cairo_move_to(_cr, center - text_width / 2.0, text_top); + } + + pango_cairo_show_layout(_cr, layout); + g_object_unref(layout); + } + } +} + +/** + * Called after all the bars have been drawn, this triggers a refresh event to + * draw it to the window. + */ +void GtkStatsTimeline:: +end_draw() { + gtk_widget_queue_draw(_graph_window); + + if (_threads_changed) { + // Make sure the window is large enough to fit all of the threads. + int num_rows = _threads.back()._row_offset + _threads.back()._rows.size(); + double height = row_to_pixel(num_rows) + _pixel_scale * 2.5; + gtk_widget_set_size_request(_graph_window, 0, height / _cr_scale); + + // Calculate the size of the thread area. + PangoLayout *layout = gtk_widget_create_pango_layout(_thread_area, ""); + + int max_width = 0; + for (const ThreadRow &thread_row : _threads) { + pango_layout_set_text(layout, thread_row._label.c_str(), thread_row._label.size()); + + int width, height; + pango_layout_get_pixel_size(layout, &width, &height); + + if (width > max_width) { + max_width = width; + } + } + + gtk_widget_set_size_request(_thread_area, max_width + _pixel_scale * 2, 0); + g_object_unref(layout); + gtk_widget_queue_draw(_thread_area); + _threads_changed = false; + } + + if (_guide_bars_changed) { + gtk_widget_queue_draw(_scale_area); + _guide_bars_changed = false; + } +} + +/** + * Called at the end of the draw cycle. + */ +void GtkStatsTimeline:: +idle() { +} + +/** + * Overridden by a derived class to implement an animation. If it returns + * false, the animation timer is stopped. + */ +bool GtkStatsTimeline:: +animate(double time, double dt) { + return PStatTimeline::animate(time, dt); +} + +/** + * This is called during the servicing of the draw event; it gives a derived + * class opportunity to do some further painting into the graph window. + */ +void GtkStatsTimeline:: +additional_graph_window_paint(cairo_t *cr) { +} + +/** + * Called when the mouse hovers over the graph, and should return the text that + * should appear on the tooltip. + */ +std::string GtkStatsTimeline:: +get_graph_tooltip(int mouse_x, int mouse_y) const { + return PStatTimeline::get_bar_tooltip(pixel_to_row(mouse_y), mouse_x); +} + +/** + * Based on the mouse position within the graph window, look for draggable + * things the mouse might be hovering over and return the appropriate DragMode + * enum or DM_none if nothing is indicated. + */ +GtkStatsGraph::DragMode GtkStatsTimeline:: +consider_drag_start(int graph_x, int graph_y) { + return GtkStatsGraph::consider_drag_start(graph_x, graph_y); +} + +/** + * Called when the mouse button is depressed within the graph window. + */ +gboolean GtkStatsTimeline:: +handle_button_press(int graph_x, int graph_y, bool double_click, int button) { + if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) { + if (button == 3) { + // Right-clicking a color bar brings up a context menu. + int row = pixel_to_row(graph_y); + + ColorBar bar; + if (find_bar(row, graph_x, bar)) { + GtkWidget *menu = gtk_menu_new(); + _popup_bar = bar; + + std::string label = get_bar_tooltip(row, graph_x); + if (!label.empty()) { + GtkWidget *menu_item = gtk_menu_item_new_with_label(label.c_str()); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + gtk_widget_set_sensitive(menu_item, FALSE); + } + + { + GtkWidget *menu_item = gtk_menu_item_new_with_label("Zoom To"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(+[] (GtkWidget *widget, gpointer data) { + GtkStatsTimeline *self = (GtkStatsTimeline *)data; + const ColorBar &bar = self->_popup_bar; + double width = bar._end - bar._start; + self->zoom_to(width * 1.5, (bar._end + bar._start) / 2.0); + self->scroll_to(bar._start - width / 4.0); + self->start_animation(); + }), + this); + } + + { + const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({ + bar._thread_index, bar._collector_index, + GtkStatsMonitor::CT_strip_chart, false, + }); + + GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Strip Chart"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(GtkStatsMonitor::menu_activate), + (void *)menu_def); + } + + { + const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({ + bar._thread_index, bar._collector_index, + GtkStatsMonitor::CT_flame_graph, + }); + + GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Flame Graph"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(GtkStatsMonitor::menu_activate), + (void *)menu_def); + } + + { + const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({ + bar._thread_index, -1, GtkStatsMonitor::CT_piano_roll, + }); + + GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Piano Roll"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + g_signal_connect(G_OBJECT(menu_item), "activate", + G_CALLBACK(GtkStatsMonitor::menu_activate), + (void *)menu_def); + } + + gtk_widget_show_all(menu); + gtk_menu_popup_at_pointer(GTK_MENU(menu), nullptr); + return TRUE; + } + return FALSE; + } + else if (double_click && button == 1) { + // Double-clicking on a color bar in the graph will zoom the graph into + // that collector. + int row = pixel_to_row(graph_y); + ColorBar bar; + if (find_bar(row, graph_x, bar)) { + double width = bar._end - bar._start; + zoom_to(width * 1.5, pixel_to_timestamp(graph_x)); + scroll_to(bar._start - width / 4.0); + } else { + // Double-clicking the white area zooms out. + _zoom_speed -= 100.0; + } + start_animation(); + } + + if (_potential_drag_mode == DM_none) { + set_drag_mode(DM_pan); + _drag_start_x = graph_x; + _scroll_speed = 0.0; + _zoom_center = pixel_to_timestamp(graph_x); + return TRUE; + } + } + + return GtkStatsGraph::handle_button_press(graph_x, graph_y, + double_click, button); +} + +/** + * Called when the mouse button is released within the graph window. + */ +gboolean GtkStatsTimeline:: +handle_button_release(int graph_x, int graph_y) { + if (_drag_mode == DM_scale) { + set_drag_mode(DM_none); + // ReleaseCapture(); + return handle_motion(graph_x, graph_y); + } + else if (_drag_mode == DM_guide_bar) { + if (graph_x < 0 || graph_x >= get_xsize()) { + remove_user_guide_bar(_drag_guide_bar); + } else { + move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_x)); + } + set_drag_mode(DM_none); + // ReleaseCapture(); + return handle_motion(graph_x, graph_y); + } + + return GtkStatsGraph::handle_button_release(graph_x, graph_y); +} + +/** + * Called when the mouse is moved within the graph window. + */ +gboolean GtkStatsTimeline:: +handle_motion(int graph_x, int graph_y) { + if (_drag_mode == DM_none && _potential_drag_mode == DM_none && + graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) { + // When the mouse is over a color bar, highlight it. + int row = pixel_to_row(graph_y); + std::swap(_highlighted_x, graph_x); + std::swap(_highlighted_row, row); + + if (row >= 0) { + PStatTimeline::force_redraw(row, graph_x, graph_x); + } + PStatTimeline::force_redraw(_highlighted_row, _highlighted_x, _highlighted_x); + + if ((_keys_held & (F_w | F_s)) != 0) { + // Update the zoom center if we move the mouse while zooming with the + // keyboard. + _zoom_center = pixel_to_timestamp(graph_x); + } + } + else { + // If the mouse is in some drag mode, stop highlighting. + if (_highlighted_row != -1) { + int row = _highlighted_row; + _highlighted_row = -1; + PStatTimeline::force_redraw(row, _highlighted_x, _highlighted_x); + } + } + + if (_drag_mode == DM_pan) { + int delta = _drag_start_x - graph_x; + _drag_start_x = graph_x; + set_horizontal_scroll(get_horizontal_scroll() + pixel_to_height(delta)); + return 0; + } + + return GtkStatsGraph::handle_motion(graph_x, graph_y); +} + +/** + * Called when the mouse has left the graph window. + */ +gboolean GtkStatsTimeline:: +handle_leave() { + if (_highlighted_row != -1) { + int row = _highlighted_row; + _highlighted_row = -1; + PStatTimeline::force_redraw(row, _highlighted_x, _highlighted_x); + } + return TRUE; +} + +/** + * + */ +gboolean GtkStatsTimeline:: +handle_scroll(int graph_x, int graph_y, double dx, double dy, bool ctrl_held) { + gboolean handled = FALSE; + + if (ctrl_held && dy != 0.0) { + handled = TRUE; + zoom_by(dy, pixel_to_timestamp(graph_x)); + start_animation(); + } + + if (dx != 0.0) { + _scroll_speed += dx * 10.0; + handled = TRUE; + start_animation(); + } + + return handled; +} + +/** + * + */ +gboolean GtkStatsTimeline:: +handle_key(bool pressed, guint val, guint16 hw_code) { + // Accept WASD based on their position rather than their mapping + int flag = 0; + switch (hw_code) { + case 25: + flag = F_w; + break; + case 38: + flag = F_a; + break; + case 39: + flag = F_s; + break; + case 40: + flag = F_d; + break; + } + if (flag == 0) { + switch (val) { + case GDK_KEY_Left: + flag = F_left; + break; + case GDK_KEY_Right: + flag = F_right; + break; + case GDK_KEY_w: + flag = F_w; + break; + case GDK_KEY_a: + flag = F_a; + break; + case GDK_KEY_s: + flag = F_s; + break; + case GDK_KEY_d: + flag = F_d; + break; + } + } + if (flag != 0) { + if (pressed) { + if (flag & (F_w | F_s)) { + // Pfoo, GTK sure does make it hard to just get the cursor position. + GdkWindow *window = gtk_widget_get_window(_graph_window); + GdkDisplay *display = gdk_window_get_display(window); + GdkDeviceManager *device_manager = gdk_display_get_device_manager(display); + GdkDevice *device = gdk_device_manager_get_client_pointer(device_manager); + gint x, y; + gdk_window_get_device_position(window, device, &x, &y, nullptr); + _zoom_center = pixel_to_timestamp(x * _cr_scale); + } + if (_keys_held == 0) { + start_animation(); + } + _keys_held |= flag; + } + else if (_keys_held != 0) { + _keys_held &= ~flag; + } + return TRUE; + } + return FALSE; +} + +/** + * This is called during the servicing of the draw event. + */ +void GtkStatsTimeline:: +draw_guide_labels(cairo_t *cr) { + int num_guide_bars = get_num_guide_bars(); + for (int i = 0; i < num_guide_bars; i++) { + draw_guide_label(cr, get_guide_bar(i)); + } +} + +/** + * Draws the text for the indicated guide bar label at the top of the graph. + */ +void GtkStatsTimeline:: +draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) { + const std::string &label = bar._label; + if (label.empty()) { + return; + } + + switch (bar._style) { + case GBS_target: + cairo_set_source_rgb(cr, rgb_light_gray[0], rgb_light_gray[1], rgb_light_gray[2]); + break; + + case GBS_user: + cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]); + break; + + case GBS_normal: + cairo_set_source_rgb(cr, rgb_light_gray[0], rgb_light_gray[1], rgb_light_gray[2]); + break; + + case GBS_frame: + cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]); + break; + } + + PangoLayout *layout = gtk_widget_create_pango_layout(_scale_area, label.c_str()); + + if (bar._style != GBS_frame) { + // Make the offsets slightly smaller. + PangoAttrList *attrs = pango_attr_list_new(); + PangoAttribute *attr = pango_attr_scale_new(0.9); + attr->start_index = 0; + attr->end_index = -1; + pango_attr_list_insert(attrs, attr); + pango_layout_set_attributes(layout, attrs); + pango_attr_list_unref(attrs); + } + + int width, height; + pango_layout_get_pixel_size(layout, &width, &height); + + int x = timestamp_to_pixel(bar._height); + if (x >= 0 && x + width * _cr_scale < get_xsize()) { + // Now convert our x to a coordinate within our drawing area. + int junk_y; + + x /= _cr_scale; + + // The x coordinate comes from the graph_window. + gtk_widget_translate_coordinates(_graph_window, _scale_area, + x, 0, &x, &junk_y); + + GtkAllocation allocation; + gtk_widget_get_allocation(_scale_area, &allocation); + + int this_x = x - width / 2; + if (this_x >= 0 && this_x + width < allocation.width) { + cairo_move_to(cr, this_x, allocation.height - height); + pango_cairo_show_layout(cr, layout); + } + } + + g_object_unref(layout); +} + +/** + * This is called during the servicing of the draw event. + */ +void GtkStatsTimeline:: +draw_thread_labels(cairo_t *cr) { + for (const ThreadRow &thread_row : _threads) { + draw_thread_label(cr, thread_row); + } +} + +/** + * Draws the text for the indicated thread on the side of the graph. + */ +void GtkStatsTimeline:: +draw_thread_label(cairo_t *cr, const ThreadRow &thread_row) { + int top = row_to_pixel(thread_row._row_offset); + if (top <= get_ysize()) { + // Now convert our y to a coordinate within our drawing area. + top /= _cr_scale; + + // The y coordinate comes from the graph_window. + int junk_x; + gtk_widget_translate_coordinates(_graph_window, _thread_area, + 0, top, &junk_x, &top); + + GtkAllocation allocation; + gtk_widget_get_allocation(_thread_area, &allocation); + + PangoLayout *layout = gtk_widget_create_pango_layout(_thread_area, thread_row._label.c_str()); + pango_layout_set_alignment(layout, PANGO_ALIGN_RIGHT); + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); + pango_layout_set_width(layout, (allocation.width - _pixel_scale * 2) * PANGO_SCALE); + + cairo_move_to(cr, _pixel_scale, top); + pango_cairo_show_layout(cr, layout); + g_object_unref(layout); + } +} + +/** + * Draws in the scale labels. + */ +gboolean GtkStatsTimeline:: +scale_area_draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) { + GtkStatsTimeline *self = (GtkStatsTimeline *)data; + self->draw_guide_labels(cr); + + return TRUE; +} + +/** + * Draws in the thread labels. + */ +gboolean GtkStatsTimeline:: +thread_area_draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) { + GtkStatsTimeline *self = (GtkStatsTimeline *)data; + self->draw_thread_labels(cr); + + return TRUE; +} + +/** + * + */ +gboolean GtkStatsTimeline:: +scroll_callback(GtkWidget *widget, GdkEventScroll *event, gpointer data) { + GtkStatsTimeline *self = (GtkStatsTimeline *)data; + + bool ctrl_held = (event->state & GDK_CONTROL_MASK) != 0; + + double dx, dy; + if (event->direction == GDK_SCROLL_UP) { + dx = 0; + dy = 1; + } + else if (event->direction == GDK_SCROLL_DOWN) { + dx = 0; + dy = -1; + } + else if (event->direction == GDK_SCROLL_LEFT) { + dx = 1; + dy = 0; + } + else if (event->direction == GDK_SCROLL_RIGHT) { + dx = -1; + dy = 0; + } + else if (!gdk_event_get_scroll_deltas((GdkEvent *)event, &dx, &dy)) { + return FALSE; + } + + int graph_x = (int)(event->x * self->_cr_scale); + int graph_y = (int)(event->y * self->_cr_scale); + return self->handle_scroll(graph_x, graph_y, dx, dy, ctrl_held); +} + +/** + * + */ +gboolean GtkStatsTimeline:: +key_press_callback(GtkWidget *widget, GdkEventKey *event, gpointer data) { + GtkStatsTimeline *self = (GtkStatsTimeline *)data; + return self->handle_key(true, event->keyval, event->hardware_keycode); +} + +/** + * + */ +gboolean GtkStatsTimeline:: +key_release_callback(GtkWidget *widget, GdkEventKey *event, gpointer data) { + GtkStatsTimeline *self = (GtkStatsTimeline *)data; + return self->handle_key(false, event->keyval, event->hardware_keycode); +} diff --git a/pandatool/src/gtk-stats/gtkStatsTimeline.h b/pandatool/src/gtk-stats/gtkStatsTimeline.h new file mode 100644 index 0000000000..4ab8d46505 --- /dev/null +++ b/pandatool/src/gtk-stats/gtkStatsTimeline.h @@ -0,0 +1,91 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file gtkStatsTimeline.h + * @author rdb + * @date 2022-02-17 + */ + +#ifndef GTKSTATSTIMELINE_H +#define GTKSTATSTIMELINE_H + +#include "pandatoolbase.h" + +#include "gtkStatsGraph.h" +#include "pStatTimeline.h" + +class GtkStatsMonitor; + +/** + * A window that draws all of the start/stop event pairs on each thread on a + * horizontal scrolling timeline, with concurrent start/stop pairs stacked + * underneath each other. + */ +class GtkStatsTimeline : public PStatTimeline, public GtkStatsGraph { +public: + GtkStatsTimeline(GtkStatsMonitor *monitor); + virtual ~GtkStatsTimeline(); + + virtual void new_data(int thread_index, int frame_number); + virtual void force_redraw(); + virtual void changed_graph_size(int graph_xsize, int graph_ysize); + +protected: + virtual void clear_region(); + virtual void begin_draw(); + virtual void draw_separator(int row); + virtual void draw_guide_bar(int x, GuideBarStyle style); + virtual void draw_bar(int row, int from_x, int to_x, int collector_index, + const std::string &collector_name); + virtual void end_draw(); + virtual void idle(); + + virtual bool animate(double time, double dt); + + virtual void additional_graph_window_paint(cairo_t *cr); + virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const; + virtual DragMode consider_drag_start(int graph_x, int graph_y); + + virtual gboolean handle_button_press(int graph_x, int graph_y, + bool double_click, int button); + virtual gboolean handle_button_release(int graph_x, int graph_y); + virtual gboolean handle_motion(int graph_x, int graph_y); + virtual gboolean handle_leave(); + gboolean handle_scroll(int graph_x, int graph_y, + double dx, double dy, bool ctrl_held); + gboolean handle_key(bool pressed, guint val, guint16 hw_code); + +private: + void draw_guide_labels(cairo_t *cr); + void draw_guide_label(cairo_t *cr, const GuideBar &bar); + void draw_thread_labels(cairo_t *cr); + void draw_thread_label(cairo_t *cr, const ThreadRow &thread_row); + + static gboolean scale_area_draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data); + static gboolean thread_area_draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data); + static gboolean scroll_callback(GtkWidget *widget, GdkEventScroll *event, gpointer data); + static gboolean key_press_callback(GtkWidget *widget, GdkEventKey *event, gpointer data); + static gboolean key_release_callback(GtkWidget *widget, GdkEventKey *event, gpointer data); + + int row_to_pixel(int y) const { + return y * _pixel_scale * 5 + _pixel_scale; + } + int pixel_to_row(int y) const { + return (y - _pixel_scale) / (_pixel_scale * 5); + } + + GtkWidget *_thread_area; + + cairo_pattern_t *_grid_pattern; + + int _highlighted_row = -1; + int _highlighted_x = 0; + ColorBar _popup_bar; +}; + +#endif diff --git a/pandatool/src/gtk-stats/gtkstats_composite1.cxx b/pandatool/src/gtk-stats/gtkstats_composite1.cxx index fefd3eeff4..3ab44d393e 100644 --- a/pandatool/src/gtk-stats/gtkstats_composite1.cxx +++ b/pandatool/src/gtk-stats/gtkstats_composite1.cxx @@ -8,3 +8,4 @@ #include "gtkStatsPianoRoll.cxx" #include "gtkStatsServer.cxx" #include "gtkStatsStripChart.cxx" +#include "gtkStatsTimeline.cxx" diff --git a/pandatool/src/pstatserver/CMakeLists.txt b/pandatool/src/pstatserver/CMakeLists.txt index 8017b7888e..020b57785a 100644 --- a/pandatool/src/pstatserver/CMakeLists.txt +++ b/pandatool/src/pstatserver/CMakeLists.txt @@ -13,6 +13,7 @@ set(P3PSTATSERVER_HEADERS pStatServer.h pStatStripChart.h pStatStripChart.I pStatThreadData.h pStatThreadData.I + pStatTimeline.h pStatTimeline.I pStatView.h pStatView.I pStatViewLevel.h pStatViewLevel.I ) @@ -22,10 +23,15 @@ set(P3PSTATSERVER_SOURCES pStatFlameGraph.cxx pStatGraph.cxx pStatListener.cxx - pStatMonitor.cxx pStatPianoRoll.cxx - pStatReader.cxx pStatServer.cxx - pStatStripChart.cxx pStatThreadData.cxx - pStatView.cxx pStatViewLevel.cxx + pStatMonitor.cxx + pStatPianoRoll.cxx + pStatReader.cxx + pStatServer.cxx + pStatStripChart.cxx + pStatThreadData.cxx + pStatTimeline.cxx + pStatView.cxx + pStatViewLevel.cxx ) composite_sources(p3pstatserver P3PSTATSERVER_SOURCES) diff --git a/pandatool/src/pstatserver/p3pstatserver_composite1.cxx b/pandatool/src/pstatserver/p3pstatserver_composite1.cxx index 079c6e6d22..a3f7c09d34 100644 --- a/pandatool/src/pstatserver/p3pstatserver_composite1.cxx +++ b/pandatool/src/pstatserver/p3pstatserver_composite1.cxx @@ -8,5 +8,6 @@ #include "pStatServer.cxx" #include "pStatStripChart.cxx" #include "pStatThreadData.cxx" +#include "pStatTimeline.cxx" #include "pStatView.cxx" #include "pStatViewLevel.cxx" diff --git a/pandatool/src/pstatserver/pStatFlameGraph.I b/pandatool/src/pstatserver/pStatFlameGraph.I index af7921ae4b..2ab06a159c 100644 --- a/pandatool/src/pstatserver/pStatFlameGraph.I +++ b/pandatool/src/pstatserver/pStatFlameGraph.I @@ -12,15 +12,15 @@ */ /** - * Returns the View this chart represents. + * Returns the particular thread whose data this flame graph reflects. */ -INLINE PStatView &PStatFlameGraph:: -get_view() const { - return _view; +INLINE int PStatFlameGraph:: +get_thread_index() const { + return _thread_index; } /** - * Returns the particular collector whose data this strip chart reflects. + * Returns the particular collector whose data this flame graph reflects. */ INLINE int PStatFlameGraph:: get_collector_index() const { @@ -41,11 +41,19 @@ get_horizontal_scale() const { * the color values over pstats_average_time seconds, which hides spikes and * makes the overall trends easier to read. When false, the strip chart shows * the actual data as it is happening. + * + * If you set this to true, you need to call animate() periodically so that the + * averages are smoothly updated over time. */ INLINE void PStatFlameGraph:: set_average_mode(bool average_mode) { if (_average_mode != average_mode) { _average_mode = average_mode; + _stack.reset_averages(); + if (!average_mode) { + _time_width = _stack.get_net_value(false); + normal_guide_bars(); + } force_redraw(); } } @@ -87,3 +95,18 @@ INLINE bool PStatFlameGraph:: is_title_unknown() const { return _title_unknown; } +/** + * Returns the net value of this stack level. + */ +INLINE double PStatFlameGraph::StackLevel:: +get_net_value(bool average) const { + if (_collector_index >= 0) { + return average ? _avg_net_value : _net_value; + } else { + double sum = 0.0; + for (auto &item : _children) { + sum += item.second.get_net_value(average); + } + return sum; + } +} diff --git a/pandatool/src/pstatserver/pStatFlameGraph.cxx b/pandatool/src/pstatserver/pStatFlameGraph.cxx index 16643345d7..c146203e63 100644 --- a/pandatool/src/pstatserver/pStatFlameGraph.cxx +++ b/pandatool/src/pstatserver/pStatFlameGraph.cxx @@ -25,12 +25,12 @@ * */ PStatFlameGraph:: -PStatFlameGraph(PStatMonitor *monitor, PStatView &view, +PStatFlameGraph(PStatMonitor *monitor, int thread_index, int collector_index, int xsize, int ysize) : PStatGraph(monitor, xsize, ysize), _thread_index(thread_index), - _view(view), - _collector_index(collector_index) + _collector_index(collector_index), + _orig_collector_index(collector_index) { _average_mode = true; _average_cursor = 0; @@ -70,8 +70,6 @@ update() { _current_frame = frame_number; update_data(); - force_redraw(); - update_labels(); } } } @@ -85,12 +83,20 @@ update() { */ void PStatFlameGraph:: set_collector_index(int collector_index) { + if (collector_index == -1) { + // First go back to the collector where we originally opened this graph, + // and only then go back to the root. + collector_index = _orig_collector_index; + if (_collector_index == _orig_collector_index) { + collector_index = -1; + _orig_collector_index = -1; + } + } if (_collector_index != collector_index) { _collector_index = collector_index; _title_unknown = true; + _stack.clear(); update_data(); - force_redraw(); - update_labels(); } } @@ -104,45 +110,61 @@ get_title_text() { _title_unknown = false; const PStatClientData *client_data = _monitor->get_client_data(); - if (client_data->has_collector(_collector_index)) { - text = client_data->get_collector_fullname(_collector_index); - text += " flame graph"; - } else { - _title_unknown = true; - } - - if (_thread_index != 0) { - if (client_data->has_thread(_thread_index)) { - text += " (" + client_data->get_thread_name(_thread_index) + " thread)"; + if (_collector_index >= 0) { + if (client_data->has_collector(_collector_index)) { + text = client_data->get_collector_fullname(_collector_index); + text += " flame graph"; } else { _title_unknown = true; } + + if (_thread_index != 0) { + if (client_data->has_thread(_thread_index)) { + text += " (" + client_data->get_thread_name(_thread_index) + " thread)"; + } else { + _title_unknown = true; + } + } + } + else if (client_data->has_thread(_thread_index)) { + text += client_data->get_thread_name(_thread_index) + " thread flame graph"; + } + else { + _title_unknown = true; } return text; } /** - * Called when the mouse hovers over a label, and should return the text that + * Called when the mouse hovers over the graph, and should return the text that * should appear on the tooltip. */ std::string PStatFlameGraph:: -get_label_tooltip(int collector_index) const { - const PStatClientData *client_data = _monitor->get_client_data(); - if (!client_data->has_collector(collector_index)) { - return std::string(); +get_bar_tooltip(int depth, int x) const { + const StackLevel *level = _stack.locate(depth, pixel_to_height(x), _average_mode); + if (level != nullptr) { + const PStatClientData *client_data = _monitor->get_client_data(); + if (client_data != nullptr && client_data->has_collector(level->_collector_index)) { + std::ostringstream text; + text << client_data->get_collector_fullname(level->_collector_index); + text << " (" << format_number(level->get_net_value(_average_mode), GBU_show_units | GBU_ms) << ")"; + return text.str(); + } } + return std::string(); +} - std::ostringstream text; - text << client_data->get_collector_fullname(collector_index); - - Data::const_iterator it = _data.find(collector_index); - if (it != _data.end()) { - const CollectorData &cd = it->second; - text << " (" << format_number(cd._net_value, get_guide_bar_units(), get_guide_bar_unit_name()) << ")"; +/** + * Returns the collector index corresponding to the bar at the given location. + */ +int PStatFlameGraph:: +get_bar_collector(int depth, int x) const { + const StackLevel *level = _stack.locate(depth, pixel_to_height(x), _average_mode); + if (level != nullptr) { + return level->_collector_index; } - - return text.str(); + return -1; } /** @@ -150,66 +172,73 @@ get_label_tooltip(int collector_index) const { */ void PStatFlameGraph:: update_data() { - // First clear the net values, so we'll know which labels should be deleted. - for (auto it = _data.begin(); it != _data.end(); ++it) { - it->second._net_value = 0; + const PStatClientData *client_data = _monitor->get_client_data(); + if (client_data == nullptr) { + return; } - _view.set_to_frame(_current_frame); - - const PStatViewLevel *level = _view.get_level(_collector_index); - double offset = 0; - update_data(level, 0, offset); - - _time_width = (offset != 0) ? offset : 1.0 / pstats_target_frame_rate; - normal_guide_bars(); - - // Cycle through the ring buffers. - _average_cursor = (_average_cursor + 1) % _num_average_frames; -} - -/** - * Recursive helper for get_frame_data. - */ -void PStatFlameGraph:: -update_data(const PStatViewLevel *level, int depth, double &offset) { - double net_value = level->get_net_value(); - - Data::iterator it; - bool inserted; - std::tie(it, inserted) = _data.insert(std::make_pair(level->get_collector(), CollectorData())); - CollectorData &cd = it->second; - cd._offset = offset; - cd._depth = depth; - - if (inserted || !_average_mode) { - // Initialize the values array. - for (double &v : cd._values) { - v = net_value; - } - cd._net_value = net_value; - } else { - cd._values[_average_cursor] = net_value; - - // Calculate the average. - cd._net_value = 0; - for (double value : cd._values) { - cd._net_value += value; - } - cd._net_value /= _num_average_frames; + const PStatThreadData *thread_data = client_data->get_thread_data(_thread_index); + if (thread_data == nullptr || thread_data->is_empty()) { + return; } - if (cd._net_value != 0.0) { - cd._net_value = std::max(cd._net_value, 0.0); + const PStatFrameData &frame_data = thread_data->get_frame(_current_frame); - double child_offset = offset; - offset += cd._net_value; + bool first_time = _stack._children.empty(); - int num_children = level->get_num_children(); - for (int i = 0; i < num_children; i++) { - const PStatViewLevel *child = level->get_child(i); - update_data(child, depth + 1, child_offset); + StackLevel *top = &_stack; + top->reset(); + + size_t num_events = frame_data.get_num_events(); + for (size_t ei = 0; ei < num_events; ++ei) { + int collector_index = frame_data.get_time_collector(ei); + double time = frame_data.get_time(ei); + + if (frame_data.is_start(ei)) { + // If we have a collector index, use it to determine which bottom-level + // stack frames we are interested in. + if (_collector_index < 0 || + _collector_index == collector_index || + top != &_stack) { + top = top->start(collector_index, time); + } + else { + // Check whether one of the parents matches, perhaps. + int parent_index = collector_index; + do { + const PStatCollectorDef &def = client_data->get_collector_def(parent_index); + if (parent_index == def._parent_index) { + break; + } + parent_index = def._parent_index; + if (parent_index == _collector_index) { + // Yes, let it through. + top = top->start(collector_index, time); + break; + } + } + while (parent_index >= 0 && client_data->has_collector(parent_index)); + } } + else { + top = top->stop(collector_index, time); + } + } + top = top->stop_all(frame_data.get_end()); + nassertv(top == &_stack); + + if (first_time) { + _stack.reset_averages(); + } + + if (!_average_mode) { + // Redraw right away, except in average mode, where it's done in animate(). + _time_width = _stack.get_net_value(false); + if (_time_width == 0.0) { + _time_width = 1.0 / pstats_target_frame_rate; + } + normal_guide_bars(); + force_redraw(); } } @@ -226,7 +255,6 @@ changed_size(int xsize, int ysize) { normal_guide_bars(); force_redraw(); - update_labels(); } } @@ -237,22 +265,10 @@ changed_size(int xsize, int ysize) { void PStatFlameGraph:: force_redraw() { begin_draw(); + r_draw_level(_stack); end_draw(); } -/** - * Resets the list of labels. - */ -void PStatFlameGraph:: -update_labels() { - for (auto it = _data.begin(); it != _data.end(); ++it) { - int collector_index = it->first; - const CollectorData &cd = it->second; - - update_label(collector_index, cd._depth, height_to_pixel(cd._offset), height_to_pixel(cd._net_value)); - } -} - /** * Calls update_guide_bars with parameters suitable to this kind of graph. */ @@ -280,6 +296,14 @@ void PStatFlameGraph:: begin_draw() { } +/** + * Should be overridden by the user class. Should draw a single bar at the + * indicated location. + */ +void PStatFlameGraph:: +draw_bar(int depth, int from_x, int to_x, int collector_index) { +} + /** * Should be overridden by the user class. This hook will be called after * drawing a series of color bars in the chart. @@ -295,3 +319,215 @@ end_draw() { void PStatFlameGraph:: idle() { } + +/** + * Should be called periodically to update any animated values. Returns false + * to indicate that the animation is done and no longer needs to be called. + */ +bool PStatFlameGraph:: +animate(double time, double dt) { + if (!_average_mode) { + return false; + } + + if (_stack.update_averages(_average_cursor)) { + _time_width = _stack.get_net_value(true); + if (_time_width == 0.0) { + _time_width = 1.0 / pstats_target_frame_rate; + } + normal_guide_bars(); + force_redraw(); + } + + // Cycle through the ring buffers. + _average_cursor = (_average_cursor + 1) % _num_average_frames; + return true; +} + +/** + * Resets all the nodes by setting their _net_value to 0.0. + */ +void PStatFlameGraph::StackLevel:: +reset() { + _start_time = 0.0; + _net_value = 0.0; + _started = false; + + for (auto &item : _children) { + item.second.reset(); + } +} + +/** + * Starts the given collector, which starts a new stack frame as a child of + * the current one. Returns the new child, which is the new stack top. + */ +PStatFlameGraph::StackLevel *PStatFlameGraph::StackLevel:: +start(int collector_index, double time) { + StackLevel &child = _children[collector_index]; + child._parent = this; + child._collector_index = collector_index; + child._start_time = std::max(_start_time, time); + child._started = true; + return &child; +} + +/** + * Stops the given collector, which is assumed to be somewhere up the + * hierarchy. Should only be called on the top of the stack, usually. + * Returns the new top of the stack. + */ +PStatFlameGraph::StackLevel *PStatFlameGraph::StackLevel:: +stop(int collector_index, double time) { + StackLevel *new_top = r_stop(collector_index, time); + if (new_top != nullptr) { + return new_top; + } + // We have a stop event without a preceding start event. Measure the + // the time from the start of the current stack frame to the stop time. + // Actually, don't do this, because it means that a child may end up with + // more time than the parent. Need a better solution for this - or not? + //start(collector_index, _start_time)->stop(collector_index, time); + return this; +} + +/** + * Stops all still started collectors. Returns the bottom of the stack, + * which is also the new top of the stack. + */ +PStatFlameGraph::StackLevel *PStatFlameGraph::StackLevel:: +stop_all(double time) { + if (_parent != nullptr) { + nassertr(_started, this); + _net_value += time - _start_time; + return _parent->stop_all(time); + } else { + return this; + } +} + +/** + * Resets the average calculator, used when first enabling average mode. + */ +void PStatFlameGraph::StackLevel:: +reset_averages() { + double net_value = get_net_value(false); + + for (double &value : _values) { + value = net_value; + } + _avg_net_value = net_value; + + for (auto &item : _children) { + item.second.reset_averages(); + } +} + +/** + * Recursively calculates the averages. Returns true if any value was changed. + */ +bool PStatFlameGraph::StackLevel:: +update_averages(size_t cursor) { + _values[cursor] = get_net_value(false); + + bool changed = false; + + double sum = 0; + for (double value : _values) { + sum += value; + } + double avg = sum / _num_average_frames; + if (avg != _avg_net_value) { + _avg_net_value = avg; + changed = true; + } + + for (auto &item : _children) { + if (item.second.update_averages(cursor)) { + changed = true; + } + } + + return changed; +} + +/** + * Locates a stack level at the given depth and the given time offset. + */ +const PStatFlameGraph::StackLevel *PStatFlameGraph::StackLevel:: +locate(int depth, double time, bool average) const { + if (time < 0.0) { + return nullptr; + } + for (const auto &item : _children) { + double value = item.second.get_net_value(average); + if (time < value) { + if (depth == 0) { + // This is it. + return &item.second; + } else { + // Recurse. + return item.second.locate(depth - 1, time, average); + } + } + time -= value; + } + return nullptr; +} + +/** + * Clears everything. + */ +void PStatFlameGraph::StackLevel:: +clear() { + _children.clear(); + _net_value = 0.0; +} + +/** + * Recursive helper used by stop(). + */ +PStatFlameGraph::StackLevel *PStatFlameGraph::StackLevel:: +r_stop(int collector_index, double time) { + if (_collector_index == collector_index) { + // Found it. + nassertr(_started, nullptr); + _net_value += time - _start_time; + _started = false; + nassertr(_parent != nullptr, nullptr); + return _parent; + } + else if (_parent != nullptr) { + StackLevel *level = _parent->r_stop(collector_index, time); + if (level != nullptr) { + nassertr(_started, nullptr); + _net_value += time - _start_time; + _started = false; + return level; + } + } + return nullptr; +} + +/** + * Recursively draws a level. + */ +void PStatFlameGraph:: +r_draw_level(const StackLevel &level, int depth, double offset) { + for (const auto &item : level._children) { + const StackLevel &child = item.second; + + double value = child.get_net_value(_average_mode); + + int from_x = height_to_pixel(offset); + int to_x = height_to_pixel(offset + value); + + // No need to recurse if the bars have become smaller than a pixel. + if (to_x > from_x) { + draw_bar(depth, from_x, to_x, child._collector_index); + r_draw_level(child, depth + 1, offset); + } + + offset += value; + } +} diff --git a/pandatool/src/pstatserver/pStatFlameGraph.h b/pandatool/src/pstatserver/pStatFlameGraph.h index 148e288af9..61a61447e2 100644 --- a/pandatool/src/pstatserver/pStatFlameGraph.h +++ b/pandatool/src/pstatserver/pStatFlameGraph.h @@ -35,14 +35,14 @@ class PStatFrameData; */ class PStatFlameGraph : public PStatGraph { public: - PStatFlameGraph(PStatMonitor *monitor, PStatView &view, + PStatFlameGraph(PStatMonitor *monitor, int thread_index, int collector_index, int xsize, int ysize); virtual ~PStatFlameGraph(); void update(); - INLINE PStatView &get_view() const; + INLINE int get_thread_index() const; INLINE int get_collector_index() const; void set_collector_index(int collector_index); @@ -56,47 +56,71 @@ public: INLINE bool is_title_unknown() const; std::string get_title_text(); - std::string get_label_tooltip(int collector_index) const; + + std::string get_bar_tooltip(int depth, int x) const; + int get_bar_collector(int depth, int x) const; protected: - static const size_t _num_average_frames = 200; - - struct CollectorData { - double _offset; - double _net_value; - int _depth; - // This is updated like a ring buffer, initialized with all the same value - // at first, then always at _average_cursor. - double _values[_num_average_frames]; - }; - typedef pmap Data; - void update_data(); - void update_data(const PStatViewLevel *level, int depth, double &offset); void changed_size(int xsize, int ysize); void force_redraw(); - virtual void update_labels(); - virtual void update_label(int collector_index, int row, int x, int width)=0; virtual void normal_guide_bars(); virtual void begin_draw(); + virtual void draw_bar(int depth, int from_x, int to_x, int collector_index); virtual void end_draw(); virtual void idle(); -private: - void compute_page(const PStatFrameData &frame_data); + bool animate(double time, double dt); -protected: +private: + static const size_t _num_average_frames = 150; + + class StackLevel { + public: + void reset(); + + StackLevel *start(int collector_index, double time); + StackLevel *stop(int collector_index, double time); + StackLevel *stop_all(double time); + + INLINE double get_net_value(bool average) const; + void reset_averages(); + bool update_averages(size_t cursor); + + const StackLevel *locate(int depth, double time, bool average) const; + + void clear(); + + private: + StackLevel *r_stop(int collector_index, double time); + + double _net_value = 0.0; + double _avg_net_value = 0.0; + + // This is updated like a ring buffer, initialized with all the same value + // at first, then always at _average_cursor. + double _values[_num_average_frames] = {0.0}; + + double _start_time = 0.0; + bool _started = false; + + int _collector_index = -1; + StackLevel *_parent = nullptr; + pmap _children; + + friend class PStatFlameGraph; + }; + + void r_draw_level(const StackLevel &level, int depth = 0, double offset = 0.0); + + StackLevel _stack; int _thread_index; - -private: - PStatView &_view; int _collector_index; + int _orig_collector_index; bool _average_mode; size_t _average_cursor; - Data _data; - double _time_width; int _current_frame; bool _title_unknown; diff --git a/pandatool/src/pstatserver/pStatGraph.cxx b/pandatool/src/pstatserver/pStatGraph.cxx index 88130ff7ab..38cb21a5c8 100644 --- a/pandatool/src/pstatserver/pStatGraph.cxx +++ b/pandatool/src/pstatserver/pStatGraph.cxx @@ -178,7 +178,13 @@ format_number(double value, int guide_bar_units, const string &unit_name) { if ((guide_bar_units & GBU_named) != 0) { // Units are whatever is specified by unit_name, not a time unit at all. - label = format_number(value); + int int_value = (int)value; + if ((double)int_value == value) { + // Probably a counter or something, don't display .0 suffix. + label = format_string(int_value); + } else { + label = format_number(value); + } if ((guide_bar_units & GBU_show_units) != 0 && !unit_name.empty()) { label += " "; label += unit_name; @@ -187,10 +193,32 @@ format_number(double value, int guide_bar_units, const string &unit_name) { } else { // Units are either milliseconds or hz, or both. if ((guide_bar_units & GBU_ms) != 0) { - double ms = value * 1000.0; - label += format_number(ms); - if ((guide_bar_units & GBU_show_units) != 0) { - label += " ms"; + if ((guide_bar_units & GBU_show_units) != 0 && + value > 0 && value < 0.000001) { + double ns = value * 1000000000.0; + label += format_number(ns); + label += " ns"; + } + else if ((guide_bar_units & GBU_show_units) != 0 && + value > 0 && value < 0.001) { + double us = value * 1000000.0; + label += format_number(us); +#ifdef _WIN32 + label += " \xb5s"; +#else + label += " us"; +#endif + } + else if ((guide_bar_units & GBU_show_units) == 0 || value < 1.0) { + double ms = value * 1000.0; + label += format_number(ms); + if ((guide_bar_units & GBU_show_units) != 0) { + label += " ms"; + } + } + else { + label += format_number(value); + label += " s"; } } diff --git a/pandatool/src/pstatserver/pStatGraph.h b/pandatool/src/pstatserver/pStatGraph.h index 5b8a2d664e..69541a86ec 100644 --- a/pandatool/src/pstatserver/pStatGraph.h +++ b/pandatool/src/pstatserver/pStatGraph.h @@ -52,6 +52,7 @@ public: GBS_normal, GBS_target, GBS_user, + GBS_frame, }; class GuideBar { diff --git a/pandatool/src/pstatserver/pStatPianoRoll.I b/pandatool/src/pstatserver/pStatPianoRoll.I index f0d3fe15cd..8f5bfcc886 100644 --- a/pandatool/src/pstatserver/pStatPianoRoll.I +++ b/pandatool/src/pstatserver/pStatPianoRoll.I @@ -11,6 +11,14 @@ * @date 2000-07-18 */ +/** + * Returns the particular thread whose data this piano roll reflects. + */ +INLINE int PStatPianoRoll:: +get_thread_index() const { + return _thread_index; +} + /** * Changes the amount of time the width of the horizontal axis represents. * This may force a redraw. diff --git a/pandatool/src/pstatserver/pStatPianoRoll.cxx b/pandatool/src/pstatserver/pStatPianoRoll.cxx index 923c3991e7..25c2b416cb 100644 --- a/pandatool/src/pstatserver/pStatPianoRoll.cxx +++ b/pandatool/src/pstatserver/pStatPianoRoll.cxx @@ -128,6 +128,20 @@ update() { idle(); } +/** + * Called when the mouse hovers over a label, and should return the text that + * should appear on the tooltip. + */ +std::string PStatPianoRoll:: +get_label_tooltip(int collector_index) const { + const PStatClientData *client_data = _monitor->get_client_data(); + if (!client_data->has_collector(collector_index)) { + return std::string(); + } + + return client_data->get_collector_fullname(collector_index); +} + /** * To be called by the user class when the widget size has changed. This * updates the chart's internal data and causes it to issue redraw commands to diff --git a/pandatool/src/pstatserver/pStatPianoRoll.h b/pandatool/src/pstatserver/pStatPianoRoll.h index e1c597dccf..6c909dc36e 100644 --- a/pandatool/src/pstatserver/pStatPianoRoll.h +++ b/pandatool/src/pstatserver/pStatPianoRoll.h @@ -43,6 +43,8 @@ public: void update(); + INLINE int get_thread_index() const; + INLINE void set_horizontal_scale(double time_width); INLINE double get_horizontal_scale() const; @@ -51,6 +53,8 @@ public: INLINE int height_to_pixel(double value) const; INLINE double pixel_to_height(int y) const; + std::string get_label_tooltip(int collector_index) const; + protected: void changed_size(int xsize, int ysize); void force_redraw(); diff --git a/pandatool/src/pstatserver/pStatStripChart.I b/pandatool/src/pstatserver/pStatStripChart.I index 35d4c86265..3e924a1bc8 100644 --- a/pandatool/src/pstatserver/pStatStripChart.I +++ b/pandatool/src/pstatserver/pStatStripChart.I @@ -19,6 +19,14 @@ get_view() const { return _view; } +/** + * Returns the particular thread whose data this strip chart reflects. + */ +INLINE int PStatStripChart:: +get_thread_index() const { + return _thread_index; +} + /** * Returns the particular collector whose data this strip chart reflects. */ diff --git a/pandatool/src/pstatserver/pStatStripChart.cxx b/pandatool/src/pstatserver/pStatStripChart.cxx index 09dadc9545..2b454edcc5 100644 --- a/pandatool/src/pstatserver/pStatStripChart.cxx +++ b/pandatool/src/pstatserver/pStatStripChart.cxx @@ -56,7 +56,7 @@ PStatStripChart(PStatMonitor *monitor, PStatView &view, _unit_name = def._level_units; } - set_default_vertical_scale(); + set_auto_vertical_scale(); } /** @@ -139,6 +139,7 @@ set_collector_index(int collector_index) { _title_unknown = true; _data.clear(); clear_label_usage(); + set_auto_vertical_scale(); force_redraw(); update_labels(); } @@ -170,26 +171,44 @@ void PStatStripChart:: set_auto_vertical_scale() { const PStatThreadData *thread_data = _view.get_thread_data(); - double max_value = 0.0; + // Calculate the median value. + std::vector values; - int frame_number = -1; - for (int x = 0; x <= _xsize; x++) { - double time = pixel_to_timestamp(x); - frame_number = - thread_data->get_frame_number_at_time(time, frame_number); + if (thread_data != nullptr && !thread_data->is_empty()) { + // Find the oldest visible frame. + double start_time = pixel_to_timestamp(0); + int oldest_frame = std::max( + thread_data->get_frame_number_at_time(start_time), + thread_data->get_oldest_frame_number()); + int latest_frame = thread_data->get_latest_frame_number(); - if (thread_data->has_frame(frame_number)) { - double net_value = get_net_value(frame_number); - max_value = max(max_value, net_value); + for (int frame_number = oldest_frame; frame_number <= latest_frame; ++frame_number) { + if (thread_data->has_frame(frame_number)) { + values.push_back(get_net_value(frame_number)); + } } } - // Ok, now we know what the max value visible in the chart is. Choose a - // scale that will show all of this sensibly. - if (max_value == 0.0) { - set_vertical_scale(1.0); + if (values.empty()) { + set_default_vertical_scale(); + return; + } + + double median; + size_t half = values.size() / 2; + if (values.size() % 2 == 0) { + std::sort(values.begin(), values.end()); + median = (values[half] + values[half + 1]) / 2.0; } else { - set_vertical_scale(max_value * 1.1); + std::nth_element(values.begin(), values.begin() + half, values.end()); + median = values[half]; + } + + if (median > 0.0) { + // Take 1.5 times the median value as the vertical scale. + set_vertical_scale(median * 1.5); + } else { + set_default_vertical_scale(); } } diff --git a/pandatool/src/pstatserver/pStatStripChart.h b/pandatool/src/pstatserver/pStatStripChart.h index f98af23ac4..5802d0baa2 100644 --- a/pandatool/src/pstatserver/pStatStripChart.h +++ b/pandatool/src/pstatserver/pStatStripChart.h @@ -46,6 +46,7 @@ public: bool first_data() const; INLINE PStatView &get_view() const; + INLINE int get_thread_index() const; INLINE int get_collector_index() const; void set_collector_index(int collector_index); diff --git a/pandatool/src/pstatserver/pStatThreadData.cxx b/pandatool/src/pstatserver/pStatThreadData.cxx index 141f927e8f..61cc92e88e 100644 --- a/pandatool/src/pstatserver/pStatThreadData.cxx +++ b/pandatool/src/pstatserver/pStatThreadData.cxx @@ -288,6 +288,14 @@ record_new_frame(int frame_number, PStatFrameData *frame_data) { } int index = frame_number - _first_frame_number; + + // It's possible to receive frames out of order. + while (index < 0) { + _frames.push_front(nullptr); + ++index; + --_first_frame_number; + } + nassertv(index >= 0 && index < (int)_frames.size()); if (_frames[index] != nullptr) { diff --git a/pandatool/src/pstatserver/pStatTimeline.I b/pandatool/src/pstatserver/pStatTimeline.I new file mode 100644 index 0000000000..00dc859f78 --- /dev/null +++ b/pandatool/src/pstatserver/pStatTimeline.I @@ -0,0 +1,139 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file pStatTimeline.I + * @author rdb + * @date 2022-02-11 + */ + +/** + * Changes the amount of time the width of the horizontal axis represents. + * This may force a redraw. + */ +INLINE void PStatTimeline:: +set_horizontal_scale(double time_width) { + double max_time_width = (_highest_end_time - _lowest_start_time) * 2.0; + time_width = std::min(time_width, max_time_width); + + double scale = time_width / get_xsize(); + if (_time_scale != scale) { + _time_scale = scale; + _target_time_scale = scale; + _zoom_speed = 0.0; + normal_guide_bars(); + force_redraw(); + } +} + +/** + * Returns the amount of total time the width of the horizontal axis + * represents. + */ +INLINE double PStatTimeline:: +get_horizontal_scale() const { + return _time_scale * get_xsize(); +} + +/** + * This may force a redraw. + */ +INLINE void PStatTimeline:: +set_horizontal_scroll(double start_time) { + start_time = std::max(std::min(start_time, _highest_end_time), _lowest_start_time); + if (_start_time != start_time) { + _start_time = start_time; + _target_start_time = start_time; + _scroll_speed = 0.0; + normal_guide_bars(); + force_redraw(); + } +} + +/** + * Returns the amount of total time the width of the horizontal axis + * represents. + */ +INLINE double PStatTimeline:: +get_horizontal_scroll() const { + return _start_time; +} + +/** + * Smoothly zooms to the given time width, around the given focal point. + */ +INLINE void PStatTimeline:: +zoom_to(double time_width, double center) { + // Don't allow zooming out to beyond 2x the size of the entire timeline. + // There's a limit of zooming beyond 1 ns per bar, there's just no point... + double max_time_width = (_highest_end_time - _lowest_start_time) * 2.0; + time_width = std::min(std::max(1e-7, time_width), max_time_width); + _target_time_scale = time_width / get_xsize(); + _zoom_center = center; + + double pivot_x = (_zoom_center - _start_time) / _time_scale; + scroll_to(_zoom_center - pivot_x * _target_time_scale); +} + +/** + * Smoothly zooms by the given amount, where 1.0 is a single "tick" of zooming + * in and -1.0 is a single "tick" of zooming out. + */ +INLINE void PStatTimeline:: +zoom_by(double amount, double center) { + zoom_to(_target_time_scale * pow(0.8, amount) * get_xsize(), center); +} + +/** + * Smoothly scrolls to the given time point. + */ +INLINE void PStatTimeline:: +scroll_to(double start_time) { + _target_start_time = std::max(std::min(start_time, _highest_end_time), _lowest_start_time); +} + +/** + * Smoothly scrolls by the given amount. + */ +INLINE void PStatTimeline:: +scroll_by(double delta) { + scroll_to(_target_start_time + delta); +} + +/** + * Converts a timestamp to a horizontal pixel offset. + */ +INLINE int PStatTimeline:: +timestamp_to_pixel(double time) const { + return (int)((double)_xsize * (time - _start_time) / get_horizontal_scale()); +} + +/** + * Converts a horizontal pixel offset to a timestamp. + */ +INLINE double PStatTimeline:: +pixel_to_timestamp(int x) const { + return _time_scale * (double)x + _start_time; +} + +/** + * Converts a value (i.e. a "height" in the strip chart) to a horizontal + * pixel offset. + */ +INLINE int PStatTimeline:: +height_to_pixel(double value) const { + return (int)((double)_xsize * value / get_horizontal_scale()); +} + +/** + * Converts a horizontal pixel offset to a value (a "height" in the strip + * chart). + */ +INLINE double PStatTimeline:: +pixel_to_height(int x) const { + return _time_scale * (double)x; +} diff --git a/pandatool/src/pstatserver/pStatTimeline.cxx b/pandatool/src/pstatserver/pStatTimeline.cxx new file mode 100644 index 0000000000..8c806447c7 --- /dev/null +++ b/pandatool/src/pstatserver/pStatTimeline.cxx @@ -0,0 +1,664 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file pStatTimeline.cxx + * @author rdb + * @date 2022-02-11 + */ + +#include "pStatTimeline.h" + +#include "pStatFrameData.h" +#include "pStatCollectorDef.h" +#include "string_utils.h" +#include "config_pstatclient.h" + +#include + +/** + * + */ +PStatTimeline:: +PStatTimeline(PStatMonitor *monitor, int xsize, int ysize) : + PStatGraph(monitor, xsize, ysize) +{ + // Default to 1 millisecond per 10 pixels. + _time_scale = 1 / 10000.0; + _target_time_scale = _time_scale; + + _guide_bar_units = GBU_ms | GBU_show_units; + + // Load in the initial data, so that the user can see everything back to the + // beginning (or as far as pstats-history goes back to). + const PStatClientData *client_data = monitor->get_client_data(); + if (client_data != nullptr) { + size_t row_offset = 0; + + for (int thread_index = 0; thread_index < client_data->get_num_threads(); ++thread_index) { + _threads.emplace_back(); + ThreadRow &thread_row = _threads.back(); + thread_row._row_offset = row_offset; + + const PStatThreadData *thread_data = client_data->get_thread_data(thread_index); + if (thread_data != nullptr) { + _threads_changed = true; + + if (!thread_data->is_empty()) { + int oldest_frame = thread_data->get_oldest_frame_number(); + int latest_frame = thread_data->get_latest_frame_number(); + + double oldest_start_time = thread_data->get_frame(oldest_frame).get_start(); + double latest_end_time = thread_data->get_frame(latest_frame).get_end(); + + if (!_have_start_time) { + _have_start_time = true; + _lowest_start_time = oldest_start_time; + } + else { + _lowest_start_time = std::min(_lowest_start_time, oldest_start_time); + } + _highest_end_time = std::max(_highest_end_time, latest_end_time); + + for (int frame = oldest_frame; frame <= latest_frame; ++frame) { + update_bars(thread_index, frame); + } + } + } + + row_offset += thread_row._rows.size() + 1; + } + } + + _start_time = _lowest_start_time; + _target_start_time = _start_time; +} + +/** + * + */ +PStatTimeline:: +~PStatTimeline() { +} + +/** + * Called as each frame's data is made available. There is no guarantee the + * frames will arrive in order, or that all of them will arrive at all. The + * monitor should be prepared to accept frames received out-of-order or + * missing. + */ +void PStatTimeline:: +new_data(int thread_index, int frame_number) { + const PStatClientData *client_data = _monitor->get_client_data(); + + if (client_data != nullptr) { + const PStatThreadData *thread_data = + client_data->get_thread_data(thread_index); + + if (thread_data != nullptr && !thread_data->is_empty()) { + const PStatFrameData &frame_data = thread_data->get_frame(frame_number); + double frame_start = frame_data.get_start(); + double frame_end = frame_data.get_end(); + + if (!_have_start_time) { + _start_time = frame_start; + _have_start_time = true; + _lowest_start_time = _start_time; + } + else if (_start_time < _lowest_start_time) { + _lowest_start_time = _start_time; + } + if (frame_end > _highest_end_time) { + _highest_end_time = frame_end; + } + + while (thread_index >= _threads.size()) { + _threads_changed = true; + if (_threads.size() == 0) { + _threads.resize(1); + } else { + _threads.resize(_threads.size() + 1); + _threads[_threads.size() - 1]._row_offset = + _threads[_threads.size() - 2]._row_offset + + _threads[_threads.size() - 2]._rows.size() + 1; + } + } + + if (update_bars(thread_index, frame_number)) { + // The number of rows was changed. + // Change the offset of all subsequent ThreadRows. + ThreadRow &thread_row = _threads[thread_index]; + size_t offset = thread_row._row_offset + thread_row._rows.size() + 1; + for (size_t ti = (size_t)(thread_index + 1); ti < _threads.size(); ++ti) { + _threads[ti]._row_offset = offset; + offset += _threads[ti]._rows.size() + 1; + } + _threads_changed = true; + normal_guide_bars(); + force_redraw(); + } + else if (frame_end >= _start_time || frame_start <= _start_time + get_horizontal_scale()) { + normal_guide_bars(); + begin_draw(); + draw_thread(thread_index, frame_start, frame_end); + end_draw(); + } + } + } + + idle(); +} + +/** + * Called by new_data(). Updates the bars without doing any drawing. Returns + * true if the number of rows was changed (forcing a full redraw), false if + * only new bars were added on the right side. + */ +bool PStatTimeline:: +update_bars(int thread_index, int frame_number) { + const PStatClientData *client_data = _monitor->get_client_data(); + const PStatThreadData *thread_data = client_data->get_thread_data(thread_index); + const PStatFrameData &frame_data = thread_data->get_frame(frame_number); + ThreadRow &thread_row = _threads[thread_index]; + thread_row._label = client_data->get_thread_name(thread_index); + bool changed_num_rows = false; + + // pair + pvector > stack; + + size_t num_events = frame_data.get_num_events(); + for (size_t i = 0; i < num_events; ++i) { + int collector_index = frame_data.get_time_collector(i); + double time = frame_data.get_time(i); + + if (frame_data.is_start(i)) { + stack.push_back(std::make_pair(collector_index, std::max(time, _start_time))); + if (stack.size() > thread_row._rows.size()) { + thread_row._rows.resize(stack.size()); + changed_num_rows = true; + } + } + else if (!stack.empty()) { + if (stack.back().first == collector_index) { + // Most likely case, ending the most recent collector that is still + // open. + double start_time = stack.back().second; + stack.pop_back(); + thread_row._rows[stack.size()].push_back({ + start_time, time, collector_index, thread_index, frame_number}); + + while (!stack.empty() && stack.back().first < 0) { + stack.pop_back(); + } + } + else { + // Unlikely case: ending a collector before a "child" has ended. + // Go back and clear the row where this collector started. + // Don't decrement the row index. + for (size_t i = 0; i < stack.size(); ++i) { + auto &item = stack[stack.size() - 1 - i]; + + if (item.first == collector_index) { + thread_row._rows[stack.size() - 1 - i].push_back({ + item.second, time, collector_index, thread_index, frame_number}); + item.first = -1; + break; + } + } + } + } + else { + // Somehow, we got an end event for a collector we didn't start. + // This shouldn't really happen, so we just ignore it. + } + } + + // Add all unclosed bars. + while (!stack.empty()) { + int collector_index = stack.back().first; + if (collector_index >= 0) { + double start_time = stack.back().second; + thread_row._rows[stack.size() - 1].push_back({ + start_time, frame_data.get_end(), + collector_index, thread_index, frame_number, + }); + } + stack.pop_back(); + } + + if (thread_row._last_frame >= 0 && frame_number < thread_row._last_frame) { + // Added a frame out of order. Resort the rows. + for (Row &row : thread_row._rows) { + std::sort(row.begin(), row.end()); + } + } else { + thread_row._last_frame = frame_number; + } + + return changed_num_rows; +} + +/** + * Called when the mouse hovers over the graph, and should return the text that + * should appear on the tooltip. + */ +std::string PStatTimeline:: +get_bar_tooltip(int row, int x) const { + ColorBar bar; + if (find_bar(row, x, bar)) { + const PStatClientData *client_data = _monitor->get_client_data(); + if (client_data != nullptr && client_data->has_collector(bar._collector_index)) { + std::ostringstream text; + text << client_data->get_collector_fullname(bar._collector_index); + text << " (" << format_number(bar._end - bar._start, GBU_show_units | GBU_ms) << ")"; + return text.str(); + } + } + return std::string(); +} + +/** + * To be called by the user class when the widget size has changed. This + * updates the chart's internal data and causes it to issue redraw commands to + * reflect the new size. + */ +void PStatTimeline:: +changed_size(int xsize, int ysize) { + if (xsize != _xsize || ysize != _ysize) { + _xsize = xsize; + _ysize = ysize; + + normal_guide_bars(); + force_redraw(); + } +} + +/** + * To be called by the user class when the whole thing needs to be redrawn for + * some reason. + */ +void PStatTimeline:: +force_redraw() { + clear_region(); + + begin_draw(); + + for (const GuideBar &bar : _guide_bars) { + int x = timestamp_to_pixel(bar._height); + if (x > 0 && x < get_xsize() - 1) { + draw_guide_bar(x, bar._style); + } + } + + double start_time = _start_time; + double end_time = start_time + get_horizontal_scale(); + + int num_rows = 0; + + for (size_t ti = 0; ti < _threads.size(); ++ti) { + ThreadRow &thread_row = _threads[ti]; + for (size_t ri = 0; ri < thread_row._rows.size(); ++ri) { + draw_row((int)ti, (int)ri, start_time, end_time); + ++num_rows; + } + draw_separator(num_rows++); + } + + end_draw(); +} + +/** + * To be called by the user class when the whole thing needs to be redrawn for + * some reason. + */ +void PStatTimeline:: +force_redraw(int row, int from_x, int to_x) { + double start_time = std::max(_start_time, pixel_to_timestamp(from_x)); + double end_time = std::min(_start_time + get_horizontal_scale(), pixel_to_timestamp(to_x)); + + begin_draw(); + + for (size_t ti = 0; ti < _threads.size(); ++ti) { + ThreadRow &thread_row = _threads[ti]; + if (thread_row._row_offset > row) { + break; + } + + int row_index = row - (int)thread_row._row_offset; + if (row_index < thread_row._rows.size()) { + draw_row((int)ti, row_index, start_time, end_time); + } + } + + end_draw(); +} + +/** + * Calls update_guide_bars with parameters suitable to this kind of graph. + */ +void PStatTimeline:: +normal_guide_bars() { + double start_time = get_horizontal_scroll(); + double time_width = get_horizontal_scale(); + double end_time = start_time + time_width; + + // We want vaguely 150 pixels between guide bars. + int max_frames = get_xsize() / 100; + int l = (int)std::floor(3.0 * log10(pixel_to_height(150)) + 0.5); + double interval = pow(10.0, std::ceil(l / 3.0)); + if ((l + 3000) % 3 == 1) { + interval /= 5; + } + else if ((l + 3000) % 3 == 2) { + interval /= 2; + } + + _guide_bars.clear(); + + // Rather than getting the client data, we look in the color bar data for + // the first row, because the client data gets wiped after a while. + if (!_threads.empty() && !_threads[0]._rows.empty()) { + const Row &row = _threads[0]._rows[0]; + + // Look for the last Frame bar with end time lower than our start time. + Row::const_iterator it = std::lower_bound(row.begin(), row.end(), ColorBar {0.0, start_time}); + while (it != row.end() && it->_collector_index != 0) { + ++it; + } + + int num_frames = 0; + + while (it != row.end() && it->_start <= end_time) { + double frame_start = it->_start; + + if (frame_start > start_time) { + if (!_guide_bars.empty() && height_to_pixel(frame_start - _guide_bars.back()._height) < 30) { + // Get rid of last label, it is in the way. + _guide_bars.back()._label.clear(); + } + std::string label = "#"; + label += format_string(it->_frame_number); + _guide_bars.push_back(GuideBar(frame_start, label, GBS_frame)); + + if (++num_frames > max_frames) { + // Forget it, this is becoming too many lines. + _guide_bars.clear(); + break; + } + } + + do { + ++it; + } + while (it != row.end() && it->_collector_index != 0); + + double frame_width; + if (it != row.end()) { + // Only go up to the start of the next frame, limiting to however much + // fits in the graph. + frame_width = std::min(it->_start - frame_start, end_time - frame_start); + } else { + // Reached the end; just continue to the end of the graph. + frame_width = end_time - frame_start; + } + + if (interval > 0.0) { + int first_bar = std::max((int)((start_time - frame_start) / interval), 1); + int num_bars = (int)std::round(frame_width / interval); + + for (int i = first_bar; i < num_bars; ++i) { + double offset = i * interval; + std::string label = "+"; + label += format_number(offset, GBU_show_units | GBU_ms); + _guide_bars.push_back(GuideBar(frame_start + offset, label, GBS_normal)); + } + } + } + } + + if (_guide_bars.empty() && interval > 0.0) { + int first_bar = std::max((int)(start_time / interval), 1); + int num_bars = (int)std::round(end_time / interval); + + for (int i = first_bar; i < num_bars; ++i) { + double time = i * interval; + std::string label = format_number(time, GBU_show_units | GBU_ms); + _guide_bars.push_back(GuideBar(time, label, GBS_frame)); + } + } + + _guide_bars_changed = true; +} + +/** + * Should be overridden by the user class to wipe out the entire strip chart + * region. + */ +void PStatTimeline:: +clear_region() { +} + +/** + * Should be overridden by the user class. This hook will be called before + * drawing any bars in the chart. + */ +void PStatTimeline:: +begin_draw() { +} + +/** + * + */ +void PStatTimeline:: +draw_thread(int thread_index, double start_time, double end_time) { + if (thread_index < 0 || (size_t)thread_index > _threads.size()) { + return; + } + + ThreadRow &thread_row = _threads[(size_t)thread_index]; + for (size_t ri = 0; ri < thread_row._rows.size(); ++ri) { + draw_row(thread_index, (int)ri, start_time, end_time); + } +} + +/** + * + */ +void PStatTimeline:: +draw_row(int thread_index, int row_index, double start_time, double end_time) { + ThreadRow &thread_row = _threads[thread_index]; + Row &row = thread_row._rows[row_index]; + + const PStatClientData *client_data = _monitor->get_client_data(); + + // Find the first element whose end time is larger than our start time. + // Then iterate until at least the end of the frame. + Row::iterator it = std::lower_bound(row.begin(), row.end(), ColorBar {0.0, start_time}); + if (it == row.end()) { + return; + } + + int frame_number = it->_frame_number; + do { + ColorBar &bar = *it; + + int from_x = timestamp_to_pixel(bar._start); + int to_x = timestamp_to_pixel(bar._end); + + if (to_x >= 0 && to_x > from_x && from_x < get_xsize()) { + if (bar._collector_index != 0) { + draw_bar(thread_row._row_offset + row_index, from_x, to_x, + bar._collector_index, + client_data->get_collector_name(bar._collector_index)); + } else { + draw_bar(thread_row._row_offset + row_index, from_x, to_x, + bar._collector_index, + std::string("Frame ") + format_string(bar._frame_number)); + } + } + + ++it; + } + while (it != row.end() && (it->_start <= end_time || it->_frame_number == frame_number)); +} + +/** + * Draws a horizontal separator. + */ +void PStatTimeline:: +draw_separator(int) { +} + +/** + * Draws a vertical guide bar. If the row is -1, draws it in all rows. + */ +void PStatTimeline:: +draw_guide_bar(int x, GuideBarStyle style) { +} + +/** + * Draws a single bar in the chart for the indicated row, in the color for the + * given collector, for the indicated horizontal pixel range. + */ +void PStatTimeline:: +draw_bar(int, int, int, int, const std::string &) { +} + +/** + * Should be overridden by the user class. This hook will be called after + * drawing a series of color bars in the chart. + */ +void PStatTimeline:: +end_draw() { +} + +/** + * Should be overridden by the user class to perform any other updates might + * be necessary after the bars have been redrawn. + */ +void PStatTimeline:: +idle() { +} + +/** + * Should be called periodically to update any animated values. Returns false + * to indicate that the animation is done and no longer needs to be called. + */ +bool PStatTimeline:: +animate(double time, double dt) { + int hmove = ((_keys_held & (F_right | F_d)) != 0) + - ((_keys_held & (F_left | F_a)) != 0); + int vmove = ((_keys_held & F_w) != 0) + - ((_keys_held & F_s) != 0); + + if (hmove > 0) { + if (_scroll_speed < 0) { + _scroll_speed = 1.0; + } + _scroll_speed += 1.0; + } + else if (hmove < 0) { + if (_scroll_speed > 0) { + _scroll_speed = -1.0; + } + _scroll_speed -= 1.0; + } + else if (_scroll_speed != 0.0) { + _scroll_speed *= std::exp(-12.0 * dt); + if (std::abs(_scroll_speed) < 0.2) { + _scroll_speed = 0.0; + } + } + + if (vmove > 0) { + if (_zoom_speed < 0) { + _zoom_speed = 1.0; + } + _zoom_speed += 1.0; + } + else if (vmove < 0) { + if (_zoom_speed > 0) { + _zoom_speed = -1.0; + } + _zoom_speed -= 1.0; + } + else if (_zoom_speed != 0.0) { + _zoom_speed *= std::exp(-12.0 * dt); + if (std::abs(_zoom_speed) < 0.2) { + _zoom_speed = 0.0; + } + } + + if (_zoom_speed != 0.0) { + zoom_to(get_horizontal_scale() * pow(0.5, _zoom_speed * dt), _zoom_center); + } + + if (_scroll_speed != 0.0) { + scroll_by(_scroll_speed * 300 * _time_scale * dt); + } + + if (_target_start_time != _start_time) { + double dist = _target_start_time - _start_time; + // When the difference is less than 2 pixels, snap to target position. + if (std::abs(dist) < _time_scale * 2) { + _start_time = _target_start_time; + } else { + dist *= 1.0 - std::exp(-12.0 * dt); + _start_time += dist; + } + } + + if (_target_time_scale != _time_scale) { + //double dist = std::log(_target_time_scale) - std::log(_time_scale); + double dist = _target_time_scale - _time_scale; + if (_target_start_time == _start_time && std::abs(dist) < 0.01) { + _time_scale = _target_time_scale; + } else { + dist *= 1.0 - std::exp(-12.0 * dt); + //_time_scale *= std::exp(dist); + _time_scale += dist; + } + } + + normal_guide_bars(); + force_redraw(); + + // Stop the animation when the speed is 0 and no key is still held. + return _keys_held != 0 + || _scroll_speed != 0 + || _zoom_speed != 0 + || _target_start_time != _start_time + || _target_time_scale != _time_scale; +} + +/** + * Return the ColorBar at the indicated position. + */ +bool PStatTimeline:: +find_bar(int row, int x, ColorBar &bar) const { + double time = pixel_to_timestamp(x); + + for (size_t ti = 0; ti < _threads.size(); ++ti) { + const ThreadRow &thread_row = _threads[ti]; + if (thread_row._row_offset > row) { + break; + } + + int row_index = row - (int)thread_row._row_offset; + if (row_index < thread_row._rows.size()) { + // Find the first element whose end time is larger than the given time. + const Row &bars = thread_row._rows[row_index]; + Row::const_iterator it = std::lower_bound(bars.begin(), bars.end(), ColorBar {time, time}); + if (it != bars.end() && it->_start <= time) { + bar = *it; + return true; + } + } + } + + return false; +} diff --git a/pandatool/src/pstatserver/pStatTimeline.h b/pandatool/src/pstatserver/pStatTimeline.h new file mode 100644 index 0000000000..0a9d0f779f --- /dev/null +++ b/pandatool/src/pstatserver/pStatTimeline.h @@ -0,0 +1,130 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file pStatTimeline.h + * @author rdb + * @date 2022-02-11 + */ + +#ifndef PSTATTIMELINE_H +#define PSTATTIMELINE_H + +#include "pandatoolbase.h" + +#include "pStatGraph.h" +#include "pStatMonitor.h" +#include "pStatClientData.h" +#include "pdeque.h" + +class PStatFrameData; + +/** + * This is an abstract class that presents the interface for drawing a piano- + * roll type chart: it shows the time spent in each of a number of collectors + * as a horizontal bar of color, with time as the horizontal axis. + * + * This class just pnages all the piano-roll logic; the actual nuts and bolts + * of drawing pixels is left to a user-derived class. + */ +class PStatTimeline : public PStatGraph { +public: + PStatTimeline(PStatMonitor *monitor, int xsize, int ysize); + virtual ~PStatTimeline(); + + void new_data(int thread_index, int frame_number); + bool update_bars(int thread_index, int frame_number); + + INLINE void set_horizontal_scale(double time_width); + INLINE double get_horizontal_scale() const; + INLINE void set_horizontal_scroll(double time_start); + INLINE double get_horizontal_scroll() const; + + INLINE void zoom_to(double time_width, double pivot); + INLINE void zoom_by(double amount, double center); + INLINE void scroll_to(double time_start); + INLINE void scroll_by(double time_start); + + INLINE int timestamp_to_pixel(double time) const; + INLINE double pixel_to_timestamp(int x) const; + INLINE int height_to_pixel(double value) const; + INLINE double pixel_to_height(int y) const; + + std::string get_bar_tooltip(int row, int x) const; + +protected: + void changed_size(int xsize, int ysize); + void force_redraw(); + void force_redraw(int row, int from_x, int to_x); + void normal_guide_bars(); + + virtual void clear_region(); + virtual void begin_draw(); + void draw_thread(int thread_index, double start_time, double end_time); + void draw_row(int thread_index, int row_index, double start_time, double end_time); + virtual void draw_separator(int row); + virtual void draw_guide_bar(int x, GuideBarStyle style); + virtual void draw_bar(int row, int from_x, int to_x, int collector_index, + const std::string &collector_name); + virtual void end_draw(); + virtual void idle(); + + bool animate(double time, double dt); + + class ColorBar { + public: + double _start, _end; + int _collector_index; + int _thread_index; + int _frame_number; + + bool operator < (const ColorBar &other) const { + return _end < other._end; + } + }; + typedef pvector Row; + typedef pvector Rows; + + bool find_bar(int row, int x, ColorBar &bar) const; + + class ThreadRow { + public: + std::string _label; + Rows _rows; + size_t _row_offset = 0; + int _last_frame = -1; + }; + typedef pvector ThreadRows; + ThreadRows _threads; + bool _threads_changed = true; + + enum KeyFlag { + F_left = 1, + F_right = 2, + F_w = 4, + F_a = 8, + F_s = 16, + F_d = 32, + }; + int _keys_held = 0; + double _scroll_speed = 0.0; + double _zoom_speed = 0.0; + double _zoom_center = 0.0; + +private: + double _time_scale; + double _start_time = 0.0; + double _lowest_start_time = 0.0; + double _highest_end_time = 0.0; + bool _have_start_time = false; + double _target_start_time = 0.0; + double _target_time_scale; +}; + +#include "pStatTimeline.I" + +#endif diff --git a/pandatool/src/pstatserver/pStatView.cxx b/pandatool/src/pstatserver/pStatView.cxx index e69959a731..7d26455e7c 100644 --- a/pandatool/src/pstatserver/pStatView.cxx +++ b/pandatool/src/pstatserver/pStatView.cxx @@ -520,15 +520,16 @@ reset_level(PStatViewLevel *level) { if (level->_parent == nullptr) { // This level didn't know its parent before, but now it does. - PStatViewLevel *parent_level = get_level(parent_index); - nassertr(parent_level != level, true); - - level->_parent = parent_level; - parent_level->_children.push_back(level); - parent_level->sort_children(_client_data); - any_changed = true; - - } else if (level->_parent->_collector != parent_index) { + if (level->_collector != 0 || parent_index != 0) { + PStatViewLevel *parent_level = get_level(parent_index); + nassertr(parent_level != level, true); + level->_parent = parent_level; + parent_level->_children.push_back(level); + parent_level->sort_children(_client_data); + any_changed = true; + } + } + else if (level->_parent->_collector != parent_index) { // This level knew about its parent, but now it's something different. PStatViewLevel *old_parent_level = level->_parent; nassertr(old_parent_level != level, true); diff --git a/pandatool/src/win-stats/CMakeLists.txt b/pandatool/src/win-stats/CMakeLists.txt index 4c10e4f560..99166f6ec9 100644 --- a/pandatool/src/win-stats/CMakeLists.txt +++ b/pandatool/src/win-stats/CMakeLists.txt @@ -14,6 +14,7 @@ set(WINSTATS_HEADERS winStatsPianoRoll.h winStatsServer.h winStatsStripChart.h + winStatsTimeline.h ) set(WINSTATS_SOURCES @@ -27,6 +28,7 @@ set(WINSTATS_SOURCES winStatsPianoRoll.cxx winStatsServer.cxx winStatsStripChart.cxx + winStatsTimeline.cxx ) composite_sources(win-stats WINSTATS_SOURCES) diff --git a/pandatool/src/win-stats/winStatsChartMenu.cxx b/pandatool/src/win-stats/winStatsChartMenu.cxx index 026e082489..71d30282f5 100644 --- a/pandatool/src/win-stats/winStatsChartMenu.cxx +++ b/pandatool/src/win-stats/winStatsChartMenu.cxx @@ -93,15 +93,31 @@ do_update() { } // Now rebuild the menu with the new set of entries. - - // The menu item(s) for the thread's frame time goes first. - add_view(_menu, view.get_top_level(), false); - - bool needs_separator = true; MENUITEMINFO mii; memset(&mii, 0, sizeof(mii)); mii.cbSize = sizeof(mii); + if (_thread_index == 0) { + // Timeline goes first. + WinStatsMonitor::MenuDef menu_def(_thread_index, -1, WinStatsMonitor::CT_timeline, false); + int menu_id = _monitor->get_menu_id(menu_def); + + mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID; + mii.fType = MFT_STRING; + mii.wID = menu_id; + mii.dwTypeData = "Timeline"; + InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii); + + mii.fMask = MIIM_FTYPE; + mii.fType = MFT_SEPARATOR; + InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii); + } + + // The menu item(s) for the thread's frame time goes second. + add_view(_menu, view.get_top_level(), false); + + bool needs_separator = true; + // And then the menu item(s) for each of the level values. const PStatClientData *client_data = _monitor->get_client_data(); int num_toplevel_collectors = client_data->get_num_toplevel_collectors(); @@ -125,24 +141,13 @@ do_update() { } } - // Also menu items for flame graph and piano roll (following a separator). + // Also menu item for piano roll (following a separator). mii.fMask = MIIM_FTYPE; mii.fType = MFT_SEPARATOR; InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii); { - WinStatsMonitor::MenuDef menu_def(_thread_index, -2, false); - int menu_id = _monitor->get_menu_id(menu_def); - - mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID; - mii.fType = MFT_STRING; - mii.wID = menu_id; - mii.dwTypeData = "Flame Graph"; - InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii); - } - - { - WinStatsMonitor::MenuDef menu_def(_thread_index, -1, false); + WinStatsMonitor::MenuDef menu_def(_thread_index, -1, WinStatsMonitor::CT_piano_roll, false); int menu_id = _monitor->get_menu_id(menu_def); mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID; @@ -164,37 +169,77 @@ add_view(HMENU parent_menu, const PStatViewLevel *view_level, bool show_level) { const PStatClientData *client_data = _monitor->get_client_data(); std::string collector_name = client_data->get_collector_name(collector); - WinStatsMonitor::MenuDef menu_def(_thread_index, collector, show_level); - int menu_id = _monitor->get_menu_id(menu_def); - MENUITEMINFO mii; memset(&mii, 0, sizeof(mii)); mii.cbSize = sizeof(mii); - mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID; - mii.fType = MFT_STRING; - mii.wID = menu_id; - mii.dwTypeData = (char *)collector_name.c_str(); - InsertMenuItem(parent_menu, GetMenuItemCount(parent_menu), TRUE, &mii); - int num_children = view_level->get_num_children(); - if (num_children > 1) { - // If the collector has more than one child, add a menu entry to go - // directly to each of its children. - HMENU submenu = CreatePopupMenu(); - std::string submenu_name = collector_name + " components"; + if (show_level && num_children == 0) { + // For a level collector without children, no point in making a submenu. + WinStatsMonitor::MenuDef menu_def(_thread_index, collector, WinStatsMonitor::CT_strip_chart, show_level); + int menu_id = _monitor->get_menu_id(menu_def); + + mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID; + mii.fType = MFT_STRING; + mii.wID = menu_id; + mii.dwTypeData = (char *)collector_name.c_str(); + InsertMenuItem(parent_menu, GetMenuItemCount(parent_menu), TRUE, &mii); + return; + } + + HMENU menu; + if (!show_level && collector == 0 && num_children == 0) { + // Root collector without children, just add the options directly to the + // parent menu. + menu = parent_menu; + } + else { + // Create a submenu. + menu = CreatePopupMenu(); mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_SUBMENU; mii.fType = MFT_STRING; - mii.hSubMenu = submenu; - mii.dwTypeData = (char *)submenu_name.c_str(); + mii.hSubMenu = menu; + mii.dwTypeData = (char *)collector_name.c_str(); InsertMenuItem(parent_menu, GetMenuItemCount(parent_menu), TRUE, &mii); + } + + { + WinStatsMonitor::MenuDef menu_def(_thread_index, collector, WinStatsMonitor::CT_strip_chart, show_level); + int menu_id = _monitor->get_menu_id(menu_def); + + mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID; + mii.fType = MFT_STRING; + mii.wID = menu_id; + mii.dwTypeData = "Open Strip Chart"; + InsertMenuItem(menu, GetMenuItemCount(menu), TRUE, &mii); + } + + if (!show_level) { + if (collector == 0 && num_children == 0) { + collector = -1; + } + + WinStatsMonitor::MenuDef menu_def(_thread_index, collector, WinStatsMonitor::CT_flame_graph); + int menu_id = _monitor->get_menu_id(menu_def); + + mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID; + mii.fType = MFT_STRING; + mii.wID = menu_id; + mii.dwTypeData = "Open Flame Graph"; + InsertMenuItem(menu, GetMenuItemCount(menu), TRUE, &mii); + } + + if (num_children > 0) { + mii.fMask = MIIM_FTYPE; + mii.fType = MFT_SEPARATOR; + InsertMenuItem(menu, GetMenuItemCount(menu), TRUE, &mii); // Reverse the order since the menus are listed from the top down; we want // to be visually consistent with the graphs, which list these labels from // the bottom up. for (int c = num_children - 1; c >= 0; c--) { - add_view(submenu, view_level->get_child(c), show_level); + add_view(menu, view_level->get_child(c), show_level); } } } diff --git a/pandatool/src/win-stats/winStatsFlameGraph.cxx b/pandatool/src/win-stats/winStatsFlameGraph.cxx index 90ad7cd280..80c3e7a2b8 100644 --- a/pandatool/src/win-stats/winStatsFlameGraph.cxx +++ b/pandatool/src/win-stats/winStatsFlameGraph.cxx @@ -18,8 +18,8 @@ #include -static const int default_flame_graph_width = 800; -static const int default_flame_graph_height = 150; +static const int default_flame_graph_width = 1085; +static const int default_flame_graph_height = 210; bool WinStatsFlameGraph::_window_class_registered = false; const char * const WinStatsFlameGraph::_window_class_name = "flame"; @@ -30,7 +30,7 @@ const char * const WinStatsFlameGraph::_window_class_name = "flame"; WinStatsFlameGraph:: WinStatsFlameGraph(WinStatsMonitor *monitor, int thread_index, int collector_index) : - PStatFlameGraph(monitor, monitor->get_view(thread_index), + PStatFlameGraph(monitor, thread_index, collector_index, monitor->get_pixel_scale() * default_flame_graph_width / 4, monitor->get_pixel_scale() * default_flame_graph_height / 4), @@ -127,34 +127,7 @@ set_time_units(int unit_mask) { */ void WinStatsFlameGraph:: on_click_label(int collector_index) { - int prev_collector_index = get_collector_index(); - if (collector_index == prev_collector_index && collector_index != 0) { - // Clicking on the top label means to go up to the parent level. - const PStatClientData *client_data = - WinStatsGraph::_monitor->get_client_data(); - if (client_data->has_collector(collector_index)) { - const PStatCollectorDef &def = - client_data->get_collector_def(collector_index); - collector_index = def._parent_index; - set_collector_index(collector_index); - } - } - else { - // Clicking on any other label means to focus on that. - set_collector_index(collector_index); - } - - // Change the root collector to show the full name. - if (prev_collector_index != collector_index) { - auto it = _labels.find(prev_collector_index); - if (it != _labels.end()) { - it->second->update_text(false); - } - it = _labels.find(collector_index); - if (it != _labels.end()) { - it->second->update_text(true); - } - } + set_collector_index(collector_index); } /** @@ -164,6 +137,11 @@ void WinStatsFlameGraph:: on_enter_label(int collector_index) { if (collector_index != _highlighted_index) { _highlighted_index = collector_index; + clear_graph_tooltip(); + + if (!get_average_mode()) { + PStatFlameGraph::force_redraw(); + } } } @@ -174,53 +152,11 @@ void WinStatsFlameGraph:: on_leave_label(int collector_index) { if (collector_index == _highlighted_index && collector_index != -1) { _highlighted_index = -1; - } -} -/** - * Called when the mouse hovers over a label, and should return the text that - * should appear on the tooltip. - */ -std::string WinStatsFlameGraph:: -get_label_tooltip(int collector_index) const { - return PStatFlameGraph::get_label_tooltip(collector_index); -} - -/** - * Repositions the labels. - */ -void WinStatsFlameGraph:: -update_labels() { - if (_graph_window) { - PStatFlameGraph::update_labels(); - } -} - -/** - * Repositions a label. If width is 0, the label should be deleted. - */ -void WinStatsFlameGraph:: -update_label(int collector_index, int row, int x, int width) { - WinStatsLabel *label; - - auto it = _labels.find(collector_index); - if (it != _labels.end()) { - if (width == 0) { - delete it->second; - _labels.erase(it); - return; + if (!get_average_mode()) { + PStatFlameGraph::force_redraw(); } - label = it->second; - } else { - if (width == 0) { - return; - } - label = new WinStatsLabel(WinStatsGraph::_monitor, this, _thread_index, collector_index, false, false); - _labels[collector_index] = label; - label->setup(_graph_window); } - - label->set_pos(x, _ysize - 2 - row * label->get_height(), std::min(width, _xsize - 2)); } /** @@ -263,6 +199,56 @@ begin_draw() { for (int i = 0; i < num_guide_bars; i++) { draw_guide_bar(_bitmap_dc, get_guide_bar(i)); } + + SelectObject(_bitmap_dc, WinStatsGraph::_monitor->get_font()); + SelectObject(_bitmap_dc, GetStockObject(NULL_PEN)); + SetBkMode(_bitmap_dc, TRANSPARENT); + SetTextAlign(_bitmap_dc, TA_LEFT | TA_TOP | TA_NOUPDATECP); +} + +/** + * Should be overridden by the user class. Should draw a single bar at the + * indicated location. + */ +void WinStatsFlameGraph:: +draw_bar(int depth, int from_x, int to_x, int collector_index) { + int bottom = get_ysize() - 1 - depth * _pixel_scale * 5; + int top = bottom - _pixel_scale * 5; + + bool is_highlighted = collector_index == _highlighted_index; + HBRUSH brush = get_collector_brush(collector_index, is_highlighted); + + if (to_x < from_x + 2) { + // It's just a tiny sliver. This is a more reliable way to draw it. + RECT rect = {from_x, top + 1, from_x + 1, bottom - 1}; + FillRect(_bitmap_dc, &rect, brush); + } + else { + SelectObject(_bitmap_dc, brush); + RoundRect(_bitmap_dc, + std::max(from_x, -_pixel_scale - 1), + top, + std::min(std::max(to_x, from_x + 1), get_xsize() + _pixel_scale), + bottom, + _pixel_scale, + _pixel_scale); + + int left = std::max(from_x, 0) + _pixel_scale / 2; + int right = std::min(to_x, get_xsize()) - _pixel_scale / 2; + + if ((to_x - from_x) >= _pixel_scale * 4) { + // Only bother drawing the text if we've got some space to draw on. + // Choose a suitable foreground color. + SetTextColor(_bitmap_dc, get_collector_text_color(collector_index, is_highlighted)); + + const PStatClientData *client_data = WinStatsGraph::_monitor->get_client_data(); + const std::string &name = client_data->get_collector_name(collector_index); + + RECT rect = {left, top, right, bottom}; + DrawText(_bitmap_dc, name.data(), name.size(), + &rect, DT_LEFT | DT_END_ELLIPSIS | DT_SINGLELINE | DT_VCENTER); + } + } } /** @@ -281,6 +267,15 @@ void WinStatsFlameGraph:: idle() { } +/** + * Overridden by a derived class to implement an animation. If it returns + * false, the animation timer is stopped. + */ +bool WinStatsFlameGraph:: +animate(double time, double dt) { + return PStatFlameGraph::animate(time, dt); +} + /** * */ @@ -300,10 +295,27 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { case BN_CLICKED: if ((HWND)lparam == _average_check_box) { int result = SendMessage(_average_check_box, BM_GETCHECK, 0, 0); - set_average_mode(result == BST_CHECKED); + if (result == BST_CHECKED) { + set_average_mode(true); + start_animation(); + } else { + set_average_mode(false); + } return 0; } break; + + case 101: + set_collector_index(_popup_index); + return 0; + + case 102: + WinStatsGraph::_monitor->open_strip_chart(get_thread_index(), _popup_index, false); + return 0; + + case 103: + WinStatsGraph::_monitor->open_flame_graph(get_thread_index(), _popup_index); + return 0; } break; @@ -331,7 +343,25 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { break; case WM_MOUSEMOVE: - if (_drag_mode == DM_new_guide_bar) { + if (_drag_mode == DM_none && _potential_drag_mode == DM_none) { + // When the mouse is over a color bar, highlight it. + int x = LOWORD(lparam); + int y = HIWORD(lparam); + + int collector_index = get_bar_collector(pixel_to_depth(y), x); + on_enter_label(collector_index); + + // Now we want to get a WM_MOUSELEAVE when the mouse leaves the graph + // window. + TRACKMOUSEEVENT tme = { + sizeof(TRACKMOUSEEVENT), + TME_LEAVE, + _graph_window, + 0 + }; + TrackMouseEvent(&tme); + } + else if (_drag_mode == DM_new_guide_bar) { // We haven't created the new guide bar yet; we won't until the mouse // comes within the graph's region. int16_t x = LOWORD(lparam); @@ -348,6 +378,13 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { } break; + case WM_MOUSELEAVE: + // When the mouse leaves the graph, stop highlighting. + if (_highlighted_index != -1) { + on_leave_label(_highlighted_index); + } + break; + case WM_LBUTTONUP: if (_drag_mode == DM_guide_bar) { int16_t x = LOWORD(lparam); @@ -364,8 +401,42 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { case WM_LBUTTONDBLCLK: { - // Clicking on whitespace in the graph goes to the parent. - on_click_label(get_collector_index()); + // Double-clicking on a color bar in the graph will zoom the graph into + // that collector. + int16_t x = LOWORD(lparam); + int16_t y = HIWORD(lparam); + set_collector_index(get_bar_collector(pixel_to_depth(y), x)); + return 0; + } + break; + + case WM_CONTEXTMENU: + { + POINT point; + if (GetCursorPos(&point)) { + POINT graph_point = point; + if (ScreenToClient(_graph_window, &graph_point)) { + int depth = pixel_to_depth(graph_point.y); + int collector_index = get_bar_collector(depth, graph_point.x); + if (collector_index >= 0) { + _popup_index = collector_index; + HMENU popup = CreatePopupMenu(); + + std::string label = get_bar_tooltip(depth, graph_point.x); + if (!label.empty()) { + AppendMenu(popup, MF_STRING | MF_DISABLED, 0, label.c_str()); + } + if (collector_index == get_collector_index()) { + AppendMenu(popup, MF_STRING | MF_DISABLED, 101, "Set as Focus"); + } else { + AppendMenu(popup, MF_STRING, 101, "Set as Focus"); + } + AppendMenu(popup, MF_STRING, 102, "Open Strip Chart"); + AppendMenu(popup, MF_STRING, 103, "Open Flame Graph"); + TrackPopupMenu(popup, TPM_LEFTBUTTON, point.x, point.y, 0, _window, nullptr); + } + } + } return 0; } break; @@ -425,6 +496,15 @@ additional_graph_window_paint(HDC hdc) { } } +/** + * Called when the mouse hovers over the graph, and should return the text that + * should appear on the tooltip. + */ +std::string WinStatsFlameGraph:: +get_graph_tooltip(int mouse_x, int mouse_y) const { + return get_bar_tooltip(pixel_to_depth(mouse_y), mouse_x); +} + /** * Based on the mouse position within the window's client area, look for * draggable things the mouse might be hovering over and return the @@ -474,6 +554,14 @@ move_graph_window(int graph_left, int graph_top, int graph_xsize, int graph_ysiz } } +/** + * Converts a pixel to a depth index. + */ +int WinStatsFlameGraph:: +pixel_to_depth(int y) const { + return (get_ysize() - 1 - y) / (_pixel_scale * 5); +} + /** * Draws the line for the indicated guide bar on the graph. */ @@ -554,6 +642,7 @@ create_window() { register_window_class(application); std::string window_title = get_title_text(); + POINT window_pos = WinStatsGraph::_monitor->get_new_window_pos(); RECT win_rect = { 0, 0, @@ -565,11 +654,13 @@ create_window() { AdjustWindowRect(&win_rect, graph_window_style, FALSE); _window = - CreateWindow(_window_class_name, window_title.c_str(), graph_window_style, - CW_USEDEFAULT, CW_USEDEFAULT, - win_rect.right - win_rect.left, - win_rect.bottom - win_rect.top, - WinStatsGraph::_monitor->get_window(), nullptr, application, 0); + CreateWindowEx(WS_EX_DLGMODALFRAME, _window_class_name, + window_title.c_str(), graph_window_style, + window_pos.x, window_pos.y, + win_rect.right - win_rect.left, + win_rect.bottom - win_rect.top, + WinStatsGraph::_monitor->get_window(), + nullptr, application, 0); if (!_window) { nout << "Could not create FlameGraph window!\n"; exit(1); @@ -586,6 +677,7 @@ create_window() { if (get_average_mode()) { SendMessage(_average_check_box, BM_SETCHECK, BST_CHECKED, 0); + start_animation(); } // Ensure that the window is on top of the stack. diff --git a/pandatool/src/win-stats/winStatsFlameGraph.h b/pandatool/src/win-stats/winStatsFlameGraph.h index 262f05979e..92dd48be36 100644 --- a/pandatool/src/win-stats/winStatsFlameGraph.h +++ b/pandatool/src/win-stats/winStatsFlameGraph.h @@ -28,7 +28,7 @@ class WinStatsLabel; class WinStatsFlameGraph : public PStatFlameGraph, public WinStatsGraph { public: WinStatsFlameGraph(WinStatsMonitor *monitor, int thread_index, - int collector_index=0); + int collector_index=-1); virtual ~WinStatsFlameGraph(); virtual void new_data(int thread_index, int frame_number); @@ -39,28 +39,30 @@ public: virtual void on_click_label(int collector_index); virtual void on_enter_label(int collector_index); virtual void on_leave_label(int collector_index); - virtual std::string get_label_tooltip(int collector_index) const; protected: - virtual void update_labels(); - virtual void update_label(int collector_index, int row, int x, int width); virtual void normal_guide_bars(); void clear_region(); virtual void begin_draw(); + virtual void draw_bar(int depth, int from_x, int to_x, int collector_index); virtual void end_draw(); virtual void idle(); + virtual bool animate(double time, double dt); + LONG window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); virtual LONG graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); virtual void additional_window_paint(HDC hdc); virtual void additional_graph_window_paint(HDC hdc); + virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const; virtual DragMode consider_drag_start(int mouse_x, int mouse_y, int width, int height); virtual void move_graph_window(int graph_left, int graph_top, int graph_xsize, int graph_ysize); private: + int pixel_to_depth(int y) const; void draw_guide_bar(HDC hdc, const GuideBar &bar); void draw_guide_label(HDC hdc, int y, const PStatGraph::GuideBar &bar); void create_window(); @@ -69,9 +71,8 @@ private: static LONG WINAPI static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); std::string _net_value_text; - pmap _labels; - HWND _average_check_box; + int _popup_index = -1; static bool _window_class_registered; static const char * const _window_class_name; diff --git a/pandatool/src/win-stats/winStatsGraph.cxx b/pandatool/src/win-stats/winStatsGraph.cxx index a95c95f46d..f57d7c1959 100644 --- a/pandatool/src/win-stats/winStatsGraph.cxx +++ b/pandatool/src/win-stats/winStatsGraph.cxx @@ -14,6 +14,7 @@ #include "winStatsGraph.h" #include "winStatsMonitor.h" #include "winStatsLabelStack.h" +#include "trueClock.h" #include "convert_srgb.h" #include @@ -21,7 +22,7 @@ #define IDC_GRAPH 100 DWORD WinStatsGraph::graph_window_style = -WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_OVERLAPPEDWINDOW | WS_VISIBLE; + WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_OVERLAPPEDWINDOW | WS_VISIBLE; /** * @@ -32,6 +33,7 @@ WinStatsGraph(WinStatsMonitor *monitor) : { _window = 0; _graph_window = 0; + _tooltip_window = 0; _sizewe_cursor = LoadCursor(nullptr, IDC_SIZEWE); _hand_cursor = LoadCursor(nullptr, IDC_HAND); _bitmap = 0; @@ -47,9 +49,11 @@ WinStatsGraph(WinStatsMonitor *monitor) : _dark_color = RGB(51, 51, 51); _light_color = RGB(154, 154, 154); _user_guide_bar_color = RGB(130, 150, 255); + _frame_guide_bar_color = RGB(255, 10, 10); _dark_pen = CreatePen(PS_SOLID, 1, _dark_color); _light_pen = CreatePen(PS_SOLID, 1, _light_color); _user_guide_bar_pen = CreatePen(PS_DASH, 1, _user_guide_bar_color); + _frame_guide_bar_pen = CreatePen(PS_DASH, 1, _frame_guide_bar_color); _drag_mode = DM_none; _potential_drag_mode = DM_none; @@ -69,12 +73,14 @@ WinStatsGraph:: DeleteObject(_dark_pen); DeleteObject(_light_pen); DeleteObject(_user_guide_bar_pen); + DeleteObject(_frame_guide_bar_pen); for (auto &item : _brushes) { DeleteObject(item.second.first); DeleteObject(item.second.second); } _brushes.clear(); + _text_colors.clear(); if (_graph_window) { DestroyWindow(_graph_window); @@ -85,6 +91,11 @@ WinStatsGraph:: DestroyWindow(_window); _window = 0; } + + if (_tooltip_window) { + DestroyWindow(_tooltip_window); + _tooltip_window = 0; + } } /** @@ -150,6 +161,13 @@ void WinStatsGraph:: on_click_label(int collector_index) { } +/** + * Called when a pop-up menu should be shown for the label. + */ +void WinStatsGraph:: +on_popup_label(int collector_index) { +} + /** * Called when the user hovers the mouse over a label. */ @@ -157,6 +175,7 @@ void WinStatsGraph:: on_enter_label(int collector_index) { if (collector_index != _highlighted_index) { _highlighted_index = collector_index; + clear_graph_tooltip(); force_redraw(); } } @@ -168,6 +187,7 @@ void WinStatsGraph:: on_leave_label(int collector_index) { if (collector_index == _highlighted_index && collector_index != -1) { _highlighted_index = -1; + clear_graph_tooltip(); force_redraw(); } } @@ -181,6 +201,16 @@ get_label_tooltip(int collector_index) const { return std::string(); } +/** + * Hides the graph tooltip. + */ +void WinStatsGraph:: +clear_graph_tooltip() { + if (_tooltip_window != 0) { + SendMessage(_tooltip_window, TTM_POP, 0, 0); + } +} + /** * Returns the window handle of the surrounding window. */ @@ -229,6 +259,28 @@ move_label_stack() { } } +/** + * Turns on the animation timer, if it hasn't already been turned on. + */ +void WinStatsGraph:: +start_animation() { + if (!_timer_running) { + TrueClock *clock = TrueClock::get_global_ptr(); + _time = clock->get_short_time(); + SetTimer(_window, 0x100, 16, nullptr); + _timer_running = true; + } +} + +/** + * Overridden by a derived class to implement an animation. If it returns + * false, the animation timer is stopped. + */ +bool WinStatsGraph:: +animate(double time, double dt) { + return false; +} + /** * Returns a brush suitable for drawing in the indicated collector's color. */ @@ -256,6 +308,29 @@ get_collector_brush(int collector_index, bool highlight) { return highlight ? hbrush : brush; } +/** + * Returns a text color suitable for the given collector. + */ +COLORREF WinStatsGraph:: +get_collector_text_color(int collector_index, bool highlight) { + TextColors::iterator tci; + tci = _text_colors.find(collector_index); + if (tci != _text_colors.end()) { + return highlight ? (*tci).second.second : (*tci).second.first; + } + + LRGBColor rgb = _monitor->get_collector_color(collector_index); + double bright = + rgb[0] * 0.2126 + + rgb[1] * 0.7152 + + rgb[2] * 0.0722; + COLORREF color = bright >= 0.5 ? RGB(0, 0, 0) : RGB(255, 255, 255); + COLORREF hcolor = bright * 0.75 >= 0.5 ? RGB(0, 0, 0) : RGB(255, 255, 255); + + _text_colors[collector_index] = std::make_pair(color, hcolor); + return highlight ? hcolor : color; +} + /** * This window_proc should be called up to by the derived classes for any * messages that are not specifically handled by the derived class. @@ -339,8 +414,9 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { case WM_MOUSEMOVE: if (_drag_mode == DM_left_margin) { int16_t x = LOWORD(lparam); - _left_margin += (x - _drag_start_x); - _drag_start_x = x; + int new_left_margin = _left_margin + (x - _drag_start_x); + _left_margin = std::max(new_left_margin, _pixel_scale * 2); + _drag_start_x = x - (new_left_margin - _left_margin); InvalidateRect(hwnd, nullptr, TRUE); move_label_stack(); return 0; @@ -407,6 +483,33 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { } break; + case WM_TIMER: + { + TrueClock *clock = TrueClock::get_global_ptr(); + double new_time = clock->get_short_time(); + if (!animate(new_time, new_time - _time)) { + KillTimer(hwnd, 0x100); + _timer_running = false; + } + _time = new_time; + } + return 0; + + case WM_NOTIFY: + switch (((LPNMHDR)lparam)->code) { + case TTN_GETDISPINFO: + { + NMTTDISPINFO &info = *(NMTTDISPINFO *)lparam; + POINT point; + if (GetCursorPos(&point) && ScreenToClient(_graph_window, &point)) { + _tooltip_text = get_graph_tooltip(point.x, point.y); + info.lpszText = (char *)_tooltip_text.c_str(); + } + } + return 0; + } + break; + default: break; } @@ -468,6 +571,15 @@ void WinStatsGraph:: additional_graph_window_paint(HDC hdc) { } +/** + * Called when the mouse hovers over the graph, and should return the text that + * should appear on the tooltip. + */ +std::string WinStatsGraph:: +get_graph_tooltip(int mouse_x, int mouse_y) const { + return std::string(); +} + /** * Based on the mouse position within the window's client area, look for * draggable things the mouse might be hovering over and return the @@ -576,6 +688,25 @@ create_graph_window() { EnableWindow(_graph_window, TRUE); SetWindowSubclass(_graph_window, &static_graph_subclass_proc, 1234, (DWORD_PTR)this); + + // Create the tooltip window. This will cause a TTN_GETDISPINFO message to + // be sent to the window to acquire the tooltip text. + _tooltip_window = CreateWindow(TOOLTIPS_CLASS, nullptr, + WS_POPUP, + CW_USEDEFAULT, CW_USEDEFAULT, + CW_USEDEFAULT, CW_USEDEFAULT, + _window, nullptr, + application, nullptr); + + if (_tooltip_window != 0) { + TOOLINFO info = { 0 }; + info.cbSize = sizeof(info); + info.uFlags = TTF_IDISHWND | TTF_SUBCLASS; + info.hwnd = _window; + info.uId = (UINT_PTR)_graph_window; + info.lpszText = LPSTR_TEXTCALLBACK; + SendMessage(_tooltip_window, TTM_ADDTOOL, 0, (LPARAM)&info); + } } /** diff --git a/pandatool/src/win-stats/winStatsGraph.h b/pandatool/src/win-stats/winStatsGraph.h index 388a78323f..1cfc1bcee0 100644 --- a/pandatool/src/win-stats/winStatsGraph.h +++ b/pandatool/src/win-stats/winStatsGraph.h @@ -40,6 +40,7 @@ public: DM_guide_bar, DM_new_guide_bar, DM_sizing, + DM_pan, }; public: @@ -57,10 +58,13 @@ public: void user_guide_bars_changed(); virtual void on_click_label(int collector_index); + virtual void on_popup_label(int collector_index); virtual void on_enter_label(int collector_index); virtual void on_leave_label(int collector_index); virtual std::string get_label_tooltip(int collector_index) const; + void clear_graph_tooltip(); + HWND get_window(); protected: @@ -69,13 +73,18 @@ protected: void setup_label_stack(); void move_label_stack(); + void start_animation(); + virtual bool animate(double time, double dt); + HBRUSH get_collector_brush(int collector_index, bool highlight = false); + COLORREF get_collector_text_color(int collector_index, bool highlight = false); LONG window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); virtual LONG graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); virtual void additional_window_paint(HDC hdc); virtual void additional_graph_window_paint(HDC hdc); + virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const; virtual DragMode consider_drag_start(int mouse_x, int mouse_y, int width, int height); virtual void set_drag_mode(DragMode drag_mode); @@ -88,10 +97,15 @@ protected: typedef pmap > Brushes; Brushes _brushes; + typedef pmap > TextColors; + TextColors _text_colors; + WinStatsMonitor *_monitor; HWND _window; HWND _graph_window; + HWND _tooltip_window; WinStatsLabelStack _label_stack; + std::string _tooltip_text; HCURSOR _sizewe_cursor; HCURSOR _hand_cursor; @@ -108,9 +122,11 @@ protected: COLORREF _dark_color; COLORREF _light_color; COLORREF _user_guide_bar_color; + COLORREF _frame_guide_bar_color; HPEN _dark_pen; HPEN _light_pen; HPEN _user_guide_bar_pen; + HPEN _frame_guide_bar_pen; DragMode _drag_mode; DragMode _potential_drag_mode; @@ -122,6 +138,9 @@ protected: bool _pause; + bool _timer_running = false; + double _time; + private: void setup_bitmap(int xsize, int ysize); void release_bitmap(); diff --git a/pandatool/src/win-stats/winStatsLabel.cxx b/pandatool/src/win-stats/winStatsLabel.cxx index 51b9cb9208..595d581a80 100644 --- a/pandatool/src/win-stats/winStatsLabel.cxx +++ b/pandatool/src/win-stats/winStatsLabel.cxx @@ -66,7 +66,7 @@ WinStatsLabel(WinStatsMonitor *monitor, WinStatsGraph *graph, } else { _fg_color = RGB(255, 255, 255); } - if (bright >= 0.5 * 0.75) { + if (bright * 0.75 >= 0.5) { _highlight_fg_color = RGB(0, 0, 0); } else { _highlight_fg_color = RGB(255, 255, 255); @@ -282,6 +282,10 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { _graph->on_click_label(_collector_index); return 0; + case WM_CONTEXTMENU: + _graph->on_popup_label(_collector_index); + return 0; + case WM_MOUSEMOVE: { // When the mouse enters the label area, highlight the label. @@ -318,13 +322,23 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { HFONT hfnt = _monitor->get_font(); SelectObject(hdc, hfnt); - SetTextAlign(hdc, (_align_right ? TA_RIGHT : TA_LEFT) | TA_TOP); + SetTextAlign(hdc, TA_LEFT | TA_TOP | TA_NOUPDATECP); SetBkMode(hdc, TRANSPARENT); SetTextColor(hdc, (_highlight || _mouse_within) ? _highlight_fg_color : _fg_color); - TextOut(hdc, _align_right ? (_width - _right_margin) : _left_margin, - _top_margin, _text.data(), _text.length()); + if (_width > 8) { + UINT format = DT_END_ELLIPSIS | DT_SINGLELINE; + if (_align_right) { + format |= DT_RIGHT; + } else { + format |= DT_LEFT; + } + + RECT margins = { _left_margin, _top_margin, _width - _right_margin, _height - _bottom_margin }; + DrawText(hdc, _text.data(), _text.length(), &margins, format); + } + EndPaint(hwnd, &ps); return 0; } diff --git a/pandatool/src/win-stats/winStatsMonitor.I b/pandatool/src/win-stats/winStatsMonitor.I index 405f0a39f5..6658a5ab70 100644 --- a/pandatool/src/win-stats/winStatsMonitor.I +++ b/pandatool/src/win-stats/winStatsMonitor.I @@ -14,10 +14,11 @@ /** * */ -WinStatsMonitor::MenuDef:: -MenuDef(int thread_index, int collector_index, bool show_level) : +INLINE WinStatsMonitor::MenuDef:: +MenuDef(int thread_index, int collector_index, ChartType chart_type, bool show_level) : _thread_index(thread_index), _collector_index(collector_index), + _chart_type(chart_type), _show_level(show_level) { } @@ -25,7 +26,7 @@ MenuDef(int thread_index, int collector_index, bool show_level) : /** * */ -bool WinStatsMonitor::MenuDef:: +INLINE bool WinStatsMonitor::MenuDef:: operator < (const MenuDef &other) const { if (_thread_index != other._thread_index) { return _thread_index < other._thread_index; @@ -33,5 +34,8 @@ operator < (const MenuDef &other) const { if (_collector_index != other._collector_index) { return _collector_index < other._collector_index; } + if (_chart_type != other._chart_type) { + return _chart_type < other._chart_type; + } return (int)_show_level < (int)other._show_level; } diff --git a/pandatool/src/win-stats/winStatsMonitor.cxx b/pandatool/src/win-stats/winStatsMonitor.cxx index 7c877c28fd..b6a80c7151 100644 --- a/pandatool/src/win-stats/winStatsMonitor.cxx +++ b/pandatool/src/win-stats/winStatsMonitor.cxx @@ -16,11 +16,16 @@ #include "winStatsStripChart.h" #include "winStatsPianoRoll.h" #include "winStatsFlameGraph.h" +#include "winStatsTimeline.h" #include "winStatsChartMenu.h" #include "winStatsMenuId.h" +#include "pStatFrameData.h" #include "pStatGraph.h" #include "pStatCollectorDef.h" -#include "indent.h" + +#include + +#include bool WinStatsMonitor::_window_class_registered = false; const char * const WinStatsMonitor::_window_class_name = "monitor"; @@ -191,13 +196,14 @@ new_thread(int thread_index) { */ void WinStatsMonitor:: new_data(int thread_index, int frame_number) { - Graphs::iterator gi; - for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) { - WinStatsGraph *graph = (*gi); + for (WinStatsGraph *graph : _graphs) { graph->new_data(thread_index, frame_number); } -} + if (thread_index == 0) { + update_status_bar(); + } +} /** * Called whenever the connection to the client has been lost. This is a @@ -230,16 +236,21 @@ idle() { const PStatThreadData *thread_data = get_client_data()->get_thread_data(0); double frame_rate = thread_data->get_frame_rate(); if (frame_rate != 0.0f) { + // The leading tab centers the text in the status bar. char buffer[128]; - sprintf(buffer, "%0.1f ms / %0.1f Hz", 1000.0f / frame_rate, frame_rate); + sprintf(buffer, "\t%0.1f ms / %0.1f Hz", 1000.0f / frame_rate, frame_rate); MENUITEMINFO mii; memset(&mii, 0, sizeof(mii)); mii.cbSize = sizeof(mii); mii.fMask = MIIM_STRING; - mii.dwTypeData = buffer; + mii.dwTypeData = buffer + 1; // chop off leading tab SetMenuItemInfo(_menu_bar, MI_frame_rate_label, FALSE, &mii); DrawMenuBar(_window); + + if (_status_bar) { + SendMessage(_status_bar, WM_SETTEXT, 0, (LPARAM)buffer); + } } } @@ -288,6 +299,18 @@ get_pixel_scale() const { return _pixel_scale; } +/** + * Returns an amount by which to offset the next window position. + */ +POINT WinStatsMonitor:: +get_new_window_pos() { + int offset = _graphs.size() * 10 * _pixel_scale; + POINT pt; + pt.x = offset + _client_origin.x; + pt.y = offset + _client_origin.y; + return pt; +} + /** * Opens a new strip chart showing the indicated data. */ @@ -319,8 +342,21 @@ open_piano_roll(int thread_index) { * Opens a new flame graph showing the indicated data. */ void WinStatsMonitor:: -open_flame_graph(int thread_index) { - WinStatsFlameGraph *graph = new WinStatsFlameGraph(this, thread_index); +open_flame_graph(int thread_index, int collector_index) { + WinStatsFlameGraph *graph = new WinStatsFlameGraph(this, thread_index, collector_index); + add_graph(graph); + + graph->set_time_units(_time_units); + graph->set_scroll_speed(_scroll_speed); + graph->set_pause(_pause); +} + +/** + * Opens a new timeline. + */ +void WinStatsMonitor:: +open_timeline() { + WinStatsTimeline *graph = new WinStatsTimeline(this); add_graph(graph); graph->set_time_units(_time_units); @@ -334,7 +370,7 @@ open_flame_graph(int thread_index) { */ const WinStatsMonitor::MenuDef &WinStatsMonitor:: lookup_menu(int menu_id) const { - static MenuDef invalid(0, 0, false); + static MenuDef invalid(0, 0, CT_strip_chart, false); int menu_index = menu_id - MI_new_chart; nassertr(menu_index >= 0 && menu_index < (int)_menu_by_id.size(), invalid); return _menu_by_id[menu_index]; @@ -516,10 +552,16 @@ create_window() { SetWindowLongPtr(_window, 0, (LONG_PTR)this); + create_status_bar(application); + // For some reason, SW_SHOWNORMAL doesn't always work, but SW_RESTORE seems // to. ShowWindow(_window, SW_RESTORE); SetForegroundWindow(_window); + + _client_origin.x = 0; + _client_origin.y = 0; + ClientToScreen(_window, &_client_origin); } /** @@ -636,6 +678,165 @@ setup_frame_rate_label() { InsertMenuItem(_menu_bar, GetMenuItemCount(_menu_bar), TRUE, &mii); } +/** + * Sets up a status bar at the bottom of the screen showing assorted level + * values. + */ +void WinStatsMonitor:: +create_status_bar(HINSTANCE application) { + _status_bar = CreateWindow(STATUSCLASSNAME, nullptr, + SBARS_SIZEGRIP | WS_CHILD | WS_VISIBLE, + 0, 0, 0, 0, + _window, (HMENU)0, application, nullptr); + + update_status_bar(); + + + ShowWindow(_status_bar, SW_SHOW); + UpdateWindow(_status_bar); + + InvalidateRect(_status_bar, NULL, TRUE); +} + +/** + * Updates the status bar. + */ +void WinStatsMonitor:: +update_status_bar() { + const PStatClientData *client_data = get_client_data(); + if (client_data == nullptr) { + return; + } + + const PStatThreadData *thread_data = get_client_data()->get_thread_data(0); + if (thread_data == nullptr || thread_data->is_empty()) { + return; + } + const PStatFrameData &frame_data = thread_data->get_latest_frame(); + + // Gather the top-level collector list. + pvector parts; + pvector collectors; + size_t total_chars = 0; + + int num_toplevel_collectors = client_data->get_num_toplevel_collectors(); + for (int tc = 0; tc < num_toplevel_collectors; tc++) { + int collector = client_data->get_toplevel_collector(tc); + if (client_data->has_collector(collector) && + client_data->get_collector_has_level(collector, 0)) { + PStatView &view = get_level_view(collector, 0); + view.set_to_frame(frame_data); + double value = view.get_net_value(); + if (value == 0.0) { + // Don't include it unless we've included it before. + if (std::find(_status_bar_collectors.begin(), _status_bar_collectors.end(), collector) == _status_bar_collectors.end()) { + continue; + } + } + + const PStatCollectorDef &def = client_data->get_collector_def(collector); + std::string text = "\t" + def._name; + text += ": " + PStatGraph::format_number(value, PStatGraph::GBU_named | PStatGraph::GBU_show_units, def._level_units); + total_chars += text.size(); + parts.push_back(text); + collectors.push_back(collector); + } + } + + size_t cur_size = _status_bar_collectors.size(); + _status_bar_collectors = std::move(collectors); + + // Allocate an array for holding the right edge coordinates. + HLOCAL hloc = LocalAlloc(LHND, sizeof(int) * (parts.size() + 1)); + PINT sizes = (PINT)LocalLock(hloc); + + // Allocate the left-most slot for the framerate indicator. + double offset = 28.0 * _pixel_scale; + sizes[0] = (int)(offset + 0.5); + + if (!parts.empty()) { + // Distribute the sizes roughly based on the number of characters. It's not + // as good as measuring the text, but it's good enough. + RECT rect; + double width_per_char = 0; + GetClientRect(_status_bar, &rect); + // Leave room for the grip. + rect.right -= _pixel_scale * 4; + width_per_char = (rect.right - rect.left - offset) / (double)total_chars; + + // If we get below a minimum width, start chopping parts off. + while (!parts.empty() && width_per_char < _pixel_scale * 1.5) { + total_chars -= parts.back().size(); + parts.pop_back(); + width_per_char = (rect.right - rect.left - offset) / (double)total_chars; + } + + if (!parts.empty()) { + for (size_t i = 0; i < parts.size(); ++i) { + offset += (parts[i].size()) * width_per_char; + sizes[i + 1] = (int)(offset + 0.5); + } + } else { + // No room for any collectors; the framerate can take up the whole width. + sizes[0] = rect.right - rect.left; + } + } + + SendMessage(_status_bar, SB_SETPARTS, (WPARAM)(parts.size() + 1), (LPARAM)sizes); + + LocalUnlock(hloc); + LocalFree(hloc); + + for (size_t i = 0; i < parts.size(); ++i) { + SendMessage(_status_bar, SB_SETTEXT, i + 1, (LPARAM)parts[i].c_str()); + } +} + +/** + * Called when someone right-clicks on a part of the status bar. + */ +void WinStatsMonitor:: +show_popup_menu(int collector) { + POINT point; + if (!GetCursorPos(&point)) { + return; + } + + const PStatClientData *client_data = get_client_data(); + if (client_data == nullptr) { + return; + } + + PStatView &level_view = get_level_view(collector, 0); + const PStatViewLevel *view_level = level_view.get_top_level(); + int num_children = view_level->get_num_children(); + if (num_children == 0) { + return; + } + + HMENU popup = CreatePopupMenu(); + + // Reverse the order since the menus are listed from the top down; we want + // to be visually consistent with the graphs, which list these labels from + // the bottom up. + for (int c = num_children - 1; c >= 0; c--) { + const PStatViewLevel *child_level = view_level->get_child(c); + + int child_collector = child_level->get_collector(); + MenuDef menu_def(0, child_collector, CT_strip_chart, true); + int menu_id = get_menu_id(menu_def); + + double value = child_level->get_net_value(); + + const PStatCollectorDef &def = client_data->get_collector_def(child_collector); + std::string text = def._name; + text += ": " + PStatGraph::format_number(value, PStatGraph::GBU_named | PStatGraph::GBU_show_units, def._level_units); + AppendMenu(popup, MF_STRING, menu_id, text.c_str()); + } + + TrackPopupMenu(popup, TPM_LEFTBUTTON, point.x, point.y, 0, _window, nullptr); +} + /** * Registers the window class for the monitor window, if it has not already * been registered. @@ -691,6 +892,71 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { close(); break; + case WM_WINDOWPOSCHANGED: + if (!_graphs.empty()) { + RECT status_bar_rect; + GetWindowRect(_status_bar, &status_bar_rect); + + RECT client_rect; + GetClientRect(_window, &client_rect); + MapWindowPoints(_window, nullptr, (POINT *)&client_rect, 2); + + int delta_x = client_rect.left - _client_origin.x; + int delta_y = client_rect.top - _client_origin.y; + _client_origin.x = client_rect.left; + _client_origin.y = client_rect.top; + + int iconic_offset = 0; + + for (WinStatsGraph *graph : _graphs) { + RECT child_rect; + HWND window = graph->get_window(); + if (GetWindowRect(window, &child_rect)) { + if (IsIconic(window)) { + // Keep it glued to the bottom-left corner of the parent window. + child_rect.left = client_rect.left + iconic_offset; + child_rect.top = client_rect.bottom - (child_rect.bottom - child_rect.top) - (status_bar_rect.bottom - status_bar_rect.top); + iconic_offset += (child_rect.right - child_rect.left); + } else { + child_rect.left += delta_x; + child_rect.top += delta_y; + } + SetWindowPos(window, 0, child_rect.left, child_rect.top, 0, 0, + SWP_NOOWNERZORDER | SWP_NOZORDER | SWP_NOSIZE | SWP_NOREDRAW | SWP_NOACTIVATE); + } + } + } + break; + + case WM_SIZE: + if (_status_bar) { + SendMessage(_status_bar, WM_SIZE, 0, 0); + update_status_bar(); + } + break; + + case WM_NOTIFY: + if (((LPNMHDR)lparam)->code == NM_DBLCLK) { + NMMOUSE &mouse = *(NMMOUSE *)lparam; + if (mouse.dwItemSpec == 0) { + open_strip_chart(0, 0, false); + } + else if (mouse.dwItemSpec >= 1 && mouse.dwItemSpec <= _status_bar_collectors.size()) { + int collector = _status_bar_collectors[mouse.dwItemSpec - 1]; + open_strip_chart(0, collector, true); + } + return TRUE; + } + else if (((LPNMHDR)lparam)->code == NM_RCLICK) { + NMMOUSE &mouse = *(NMMOUSE *)lparam; + if (mouse.dwItemSpec >= 1 && + mouse.dwItemSpec <= _status_bar_collectors.size()) { + int collector = _status_bar_collectors[mouse.dwItemSpec - 1]; + show_popup_menu(collector); + } + } + break; + case WM_COMMAND: if (HIWORD(wparam) <= 1) { int menu_id = LOWORD(wparam); @@ -750,15 +1016,23 @@ handle_menu_command(int menu_id) { default: if (menu_id >= MI_new_chart) { const MenuDef &menu_def = lookup_menu(menu_id); - if (menu_def._collector_index == -2) { - open_flame_graph(menu_def._thread_index); - } - else if (menu_def._collector_index < 0) { - open_piano_roll(menu_def._thread_index); - } - else { + switch (menu_def._chart_type) { + case CT_timeline: + open_timeline(); + break; + + case CT_strip_chart: open_strip_chart(menu_def._thread_index, menu_def._collector_index, menu_def._show_level); + break; + + case CT_flame_graph: + open_flame_graph(menu_def._thread_index, menu_def._collector_index); + break; + + case CT_piano_roll: + open_piano_roll(menu_def._thread_index); + break; } } } diff --git a/pandatool/src/win-stats/winStatsMonitor.h b/pandatool/src/win-stats/winStatsMonitor.h index 9da5034a71..9fa4ccf57f 100644 --- a/pandatool/src/win-stats/winStatsMonitor.h +++ b/pandatool/src/win-stats/winStatsMonitor.h @@ -37,13 +37,22 @@ class WinStatsChartMenu; */ class WinStatsMonitor : public PStatMonitor { public: + enum ChartType { + CT_timeline, + CT_strip_chart, + CT_flame_graph, + CT_piano_roll, + }; + class MenuDef { public: - INLINE MenuDef(int thread_index, int collector_index, bool show_level); + INLINE MenuDef(int thread_index, int collector_index, + ChartType chart_type, bool show_level = false); INLINE bool operator < (const MenuDef &other) const; int _thread_index; int _collector_index; + ChartType _chart_type; bool _show_level; }; @@ -68,10 +77,12 @@ public: HWND get_window() const; HFONT get_font() const; int get_pixel_scale() const; + POINT get_new_window_pos(); void open_strip_chart(int thread_index, int collector_index, bool show_level); void open_piano_roll(int thread_index); - void open_flame_graph(int thread_index); + void open_flame_graph(int thread_index, int collector_index = -1); + void open_timeline(); const MenuDef &lookup_menu(int menu_id) const; int get_menu_id(const MenuDef &menu_def); @@ -88,6 +99,9 @@ private: void setup_options_menu(); void setup_speed_menu(); void setup_frame_rate_label(); + void create_status_bar(HINSTANCE application); + void update_status_bar(); + void show_popup_menu(int collector); static void register_window_class(HINSTANCE application); static LONG WINAPI static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); @@ -109,6 +123,9 @@ private: HMENU _menu_bar; HMENU _options_menu; HMENU _speed_menu; + HWND _status_bar; + POINT _client_origin; + pvector _status_bar_collectors; std::string _window_title; int _time_units; double _scroll_speed; diff --git a/pandatool/src/win-stats/winStatsPianoRoll.cxx b/pandatool/src/win-stats/winStatsPianoRoll.cxx index 0b22831184..fc70f6f203 100644 --- a/pandatool/src/win-stats/winStatsPianoRoll.cxx +++ b/pandatool/src/win-stats/winStatsPianoRoll.cxx @@ -15,8 +15,8 @@ #include "winStatsMonitor.h" #include "numeric_types.h" -static const int default_piano_roll_width = 600; -static const int default_piano_roll_height = 200; +static const int default_piano_roll_width = 800; +static const int default_piano_roll_height = 400; bool WinStatsPianoRoll::_window_class_registered = false; const char * const WinStatsPianoRoll::_window_class_name = "piano"; @@ -51,7 +51,7 @@ WinStatsPianoRoll:: } /** - * Called as each frame's data is made available. There is no gurantee the + * Called as each frame's data is made available. There is no guarantee the * frames will arrive in order, or that all of them will arrive at all. The * monitor should be prepared to accept frames received out-of-order or * missing. @@ -109,6 +109,36 @@ on_click_label(int collector_index) { } } +/** + * Called when the user right-clicks on a label. + */ +void WinStatsPianoRoll:: +on_popup_label(int collector_index) { + POINT point; + if (collector_index >= 0 && GetCursorPos(&point)) { + _popup_index = collector_index; + + HMENU popup = CreatePopupMenu(); + + std::string label = get_label_tooltip(collector_index); + if (!label.empty()) { + AppendMenu(popup, MF_STRING | MF_DISABLED, 0, label.c_str()); + } + AppendMenu(popup, MF_STRING, 102, "Open Strip Chart"); + AppendMenu(popup, MF_STRING, 103, "Open Flame Graph"); + TrackPopupMenu(popup, TPM_LEFTBUTTON, point.x, point.y, 0, _window, nullptr); + } +} + +/** + * Called when the mouse hovers over a label, and should return the text that + * should appear on the tooltip. + */ +std::string WinStatsPianoRoll:: +get_label_tooltip(int collector_index) const { + return PStatPianoRoll::get_label_tooltip(collector_index); +} + /** * Changes the amount of time the width of the horizontal axis represents. * This may force a redraw. @@ -216,6 +246,18 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { } break; + case WM_COMMAND: + switch (LOWORD(wparam)) { + case 102: + WinStatsGraph::_monitor->open_strip_chart(get_thread_index(), _popup_index, false); + return 0; + + case 103: + WinStatsGraph::_monitor->open_flame_graph(get_thread_index(), _popup_index); + return 0; + } + break; + default: break; } @@ -333,6 +375,19 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { } break; + case WM_CONTEXTMENU: + { + POINT point; + if (GetCursorPos(&point) && ScreenToClient(_graph_window, &point)) { + int collector_index = get_collector_under_pixel(point.x, point.y); + if (collector_index >= 0) { + on_popup_label(collector_index); + } + } + return 0; + } + break; + default: break; } @@ -379,6 +434,19 @@ additional_graph_window_paint(HDC hdc) { } } +/** + * Called when the mouse hovers over the graph, and should return the text that + * should appear on the tooltip. + */ +std::string WinStatsPianoRoll:: +get_graph_tooltip(int mouse_x, int mouse_y) const { + int collector_index = get_collector_under_pixel(mouse_x, mouse_y); + if (collector_index >= 0) { + return get_label_tooltip(collector_index); + } + return std::string(); +} + /** * Based on the mouse position within the window's client area, look for * draggable things the mouse might be hovering over and return the @@ -413,7 +481,7 @@ consider_drag_start(int mouse_x, int mouse_y, int width, int height) { * -1. */ int WinStatsPianoRoll:: -get_collector_under_pixel(int xpoint, int ypoint) { +get_collector_under_pixel(int xpoint, int ypoint) const { if (_label_stack.get_num_labels() == 0) { return -1; } @@ -521,7 +589,7 @@ create_window() { WinStatsGraph::_monitor->get_client_data(); std::string thread_name = client_data->get_thread_name(_thread_index); std::string window_title = thread_name + " thread piano roll"; - + POINT window_pos = WinStatsGraph::_monitor->get_new_window_pos(); RECT win_rect = { 0, 0, @@ -533,11 +601,13 @@ create_window() { AdjustWindowRect(&win_rect, graph_window_style, FALSE); _window = - CreateWindow(_window_class_name, window_title.c_str(), graph_window_style, - CW_USEDEFAULT, CW_USEDEFAULT, - win_rect.right - win_rect.left, - win_rect.bottom - win_rect.top, - WinStatsGraph::_monitor->get_window(), nullptr, application, 0); + CreateWindowEx(WS_EX_DLGMODALFRAME, _window_class_name, + window_title.c_str(), graph_window_style, + window_pos.x, window_pos.y, + win_rect.right - win_rect.left, + win_rect.bottom - win_rect.top, + WinStatsGraph::_monitor->get_window(), + nullptr, application, 0); if (!_window) { nout << "Could not create PianoRoll window!\n"; exit(1); diff --git a/pandatool/src/win-stats/winStatsPianoRoll.h b/pandatool/src/win-stats/winStatsPianoRoll.h index 5a5a6f1f45..1fc3ac2443 100644 --- a/pandatool/src/win-stats/winStatsPianoRoll.h +++ b/pandatool/src/win-stats/winStatsPianoRoll.h @@ -42,6 +42,8 @@ public: virtual void set_time_units(int unit_mask); virtual void on_click_label(int collector_index); + virtual void on_popup_label(int collector_index); + virtual std::string get_label_tooltip(int collector_index) const; void set_horizontal_scale(double time_width); protected: @@ -57,11 +59,12 @@ protected: virtual LONG graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); virtual void additional_window_paint(HDC hdc); virtual void additional_graph_window_paint(HDC hdc); + virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const; virtual DragMode consider_drag_start(int mouse_x, int mouse_y, int width, int height); private: - int get_collector_under_pixel(int xpoint, int ypoint); + int get_collector_under_pixel(int xpoint, int ypoint) const; void update_labels(); void draw_guide_bar(HDC hdc, const GuideBar &bar); void draw_guide_label(HDC hdc, int y, const PStatGraph::GuideBar &bar); @@ -71,6 +74,8 @@ private: static LONG WINAPI static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); + int _popup_index = -1; + static bool _window_class_registered; static const char * const _window_class_name; }; diff --git a/pandatool/src/win-stats/winStatsStripChart.cxx b/pandatool/src/win-stats/winStatsStripChart.cxx index a7cf195a63..b20b2d23aa 100644 --- a/pandatool/src/win-stats/winStatsStripChart.cxx +++ b/pandatool/src/win-stats/winStatsStripChart.cxx @@ -18,8 +18,6 @@ #include -using std::string; - static const int default_strip_chart_width = 400; static const int default_strip_chart_height = 100; @@ -33,7 +31,7 @@ WinStatsStripChart:: WinStatsStripChart(WinStatsMonitor *monitor, int thread_index, int collector_index, bool show_level) : PStatStripChart(monitor, - show_level ? monitor->get_level_view(collector_index, thread_index) : monitor->get_view(thread_index), + show_level ? monitor->get_level_view(0, thread_index) : monitor->get_view(thread_index), thread_index, collector_index, monitor->get_pixel_scale() * default_strip_chart_width / 4, @@ -82,7 +80,7 @@ new_collector(int collector_index) { } /** - * Called as each frame's data is made available. There is no gurantee the + * Called as each frame's data is made available. There is no guarantee the * frames will arrive in order, or that all of them will arrive at all. The * monitor should be prepared to accept frames received out-of-order or * missing. @@ -90,7 +88,7 @@ new_collector(int collector_index) { void WinStatsStripChart:: new_data(int thread_index, int frame_number) { if (is_title_unknown()) { - string window_title = get_title_text(); + std::string window_title = get_title_text(); if (!is_title_unknown()) { SetWindowText(_window, window_title.c_str()); } @@ -99,7 +97,7 @@ new_data(int thread_index, int frame_number) { if (!_pause) { update(); - string text = format_number(get_average_net_value(), get_guide_bar_units(), get_guide_bar_unit_name()); + std::string text = format_number(get_average_net_value(), get_guide_bar_units(), get_guide_bar_unit_name()); if (_net_value_text != text) { _net_value_text = text; RECT rect; @@ -193,6 +191,36 @@ on_click_label(int collector_index) { } } +/** + * Called when the user right-clicks on a label. + */ +void WinStatsStripChart:: +on_popup_label(int collector_index) { + POINT point; + if (collector_index >= 0 && GetCursorPos(&point)) { + _popup_index = collector_index; + + HMENU popup = CreatePopupMenu(); + + std::string label = get_label_tooltip(collector_index); + if (!label.empty()) { + AppendMenu(popup, MF_STRING | MF_DISABLED, 0, label.c_str()); + } + if (collector_index == 0 && get_collector_index() == 0) { + AppendMenu(popup, MF_STRING | MF_DISABLED, 101, "Set as Focus"); + } else { + AppendMenu(popup, MF_STRING, 101, "Set as Focus"); + } + AppendMenu(popup, MF_STRING, 102, "Open Strip Chart"); + if (get_view().get_show_level()) { + AppendMenu(popup, MF_STRING | MF_DISABLED, 103, "Open Flame Graph"); + } else { + AppendMenu(popup, MF_STRING, 103, "Open Flame Graph"); + } + TrackPopupMenu(popup, TPM_LEFTBUTTON, point.x, point.y, 0, _window, nullptr); + } +} + /** * Called when the mouse hovers over a label, and should return the text that * should appear on the tooltip. @@ -355,6 +383,19 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { return 0; } break; + + case 101: + set_collector_index(_popup_index); + break; + + case 102: + WinStatsGraph::_monitor->open_strip_chart(get_thread_index(), _popup_index, + get_view().get_show_level()); + return 0; + + case 103: + WinStatsGraph::_monitor->open_flame_graph(get_thread_index(), _popup_index); + return 0; } break; @@ -416,9 +457,14 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { if (_drag_mode == DM_scale) { int16_t y = HIWORD(lparam); - double ratio = 1.0f - ((double)y / (double)get_ysize()); - if (ratio > 0.0f) { - set_vertical_scale(_drag_scale_start / ratio); + double ratio = 1.0 - ((double)y / (double)get_ysize()); + if (ratio > 0.0) { + double new_scale = _drag_scale_start / ratio; + if (!IS_NEARLY_EQUAL(get_vertical_scale(), new_scale)) { + // Disable smoothing while we do this expensive operation. + set_average_mode(false); + set_vertical_scale(_drag_scale_start / ratio); + } } return 0; @@ -475,6 +521,19 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { } break; + case WM_CONTEXTMENU: + { + POINT point; + if (GetCursorPos(&point) && ScreenToClient(_graph_window, &point)) { + int collector_index = get_collector_under_pixel(point.x, point.y); + if (collector_index >= 0) { + on_popup_label(collector_index); + } + } + return 0; + } + break; + default: break; } @@ -534,6 +593,18 @@ additional_graph_window_paint(HDC hdc) { } } +/** + * Called when the mouse hovers over the graph, and should return the text that + * should appear on the tooltip. + */ +std::string WinStatsStripChart:: +get_graph_tooltip(int mouse_x, int mouse_y) const { + if (_highlighted_index != -1) { + return get_label_tooltip(_highlighted_index); + } + return std::string(); +} + /** * Based on the mouse position within the window's client area, look for * draggable things the mouse might be hovering over and return the @@ -569,16 +640,7 @@ void WinStatsStripChart:: set_drag_mode(WinStatsGraph::DragMode drag_mode) { WinStatsGraph::set_drag_mode(drag_mode); - switch (_drag_mode) { - case DM_scale: - case DM_left_margin: - case DM_right_margin: - case DM_sizing: - // Disable smoothing for these expensive operations. - set_average_mode(false); - break; - - default: + if (_drag_mode == DM_none) { // Restore smoothing according to the current setting of the check box. int result = SendMessage(_smooth_check_box, BM_GETCHECK, 0, 0); set_average_mode(result == BST_CHECKED); @@ -654,7 +716,7 @@ draw_guide_label(HDC hdc, int x, const PStatGraph::GuideBar &bar, int last_y) { } int y = height_to_pixel(bar._height); - const string &label = bar._label; + const std::string &label = bar._label; SIZE size; GetTextExtentPoint32(hdc, label.data(), label.length(), &size); @@ -691,7 +753,8 @@ create_window() { HINSTANCE application = GetModuleHandle(nullptr); register_window_class(application); - string window_title = get_title_text(); + std::string window_title = get_title_text(); + POINT window_pos = WinStatsGraph::_monitor->get_new_window_pos(); RECT win_rect = { 0, 0, @@ -703,11 +766,13 @@ create_window() { AdjustWindowRect(&win_rect, graph_window_style, FALSE); _window = - CreateWindow(_window_class_name, window_title.c_str(), graph_window_style, - CW_USEDEFAULT, CW_USEDEFAULT, - win_rect.right - win_rect.left, - win_rect.bottom - win_rect.top, - WinStatsGraph::_monitor->get_window(), nullptr, application, 0); + CreateWindowEx(WS_EX_DLGMODALFRAME, _window_class_name, + window_title.c_str(), graph_window_style, + window_pos.x, window_pos.y, + win_rect.right - win_rect.left, + win_rect.bottom - win_rect.top, + WinStatsGraph::_monitor->get_window(), + nullptr, application, 0); if (!_window) { nout << "Could not create StripChart window!\n"; exit(1); diff --git a/pandatool/src/win-stats/winStatsStripChart.h b/pandatool/src/win-stats/winStatsStripChart.h index dcff22a76e..850faa704c 100644 --- a/pandatool/src/win-stats/winStatsStripChart.h +++ b/pandatool/src/win-stats/winStatsStripChart.h @@ -44,6 +44,7 @@ public: virtual void set_time_units(int unit_mask); virtual void set_scroll_speed(double scroll_speed); virtual void on_click_label(int collector_index); + virtual void on_popup_label(int collector_index); virtual std::string get_label_tooltip(int collector_index) const; void set_vertical_scale(double value_height); @@ -62,6 +63,7 @@ protected: virtual LONG graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); virtual void additional_window_paint(HDC hdc); virtual void additional_graph_window_paint(HDC hdc); + virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const; virtual DragMode consider_drag_start(int mouse_x, int mouse_y, int width, int height); virtual void set_drag_mode(DragMode drag_mode); @@ -80,6 +82,7 @@ private: std::string _net_value_text; HWND _smooth_check_box; + int _popup_index = -1; static bool _window_class_registered; static const char * const _window_class_name; diff --git a/pandatool/src/win-stats/winStatsTimeline.cxx b/pandatool/src/win-stats/winStatsTimeline.cxx new file mode 100644 index 0000000000..225def4f89 --- /dev/null +++ b/pandatool/src/win-stats/winStatsTimeline.cxx @@ -0,0 +1,752 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file winStatsTimeline.cxx + * @author rdb + * @date 2022-02-11 + */ + +#include "winStatsTimeline.h" +#include "winStatsMonitor.h" +#include "numeric_types.h" + +static const int default_timeline_width = 1000; +static const int default_timeline_height = 500; + +bool WinStatsTimeline::_window_class_registered = false; +const char * const WinStatsTimeline::_window_class_name = "timeline"; + +/** + * + */ +WinStatsTimeline:: +WinStatsTimeline(WinStatsMonitor *monitor) : + PStatTimeline(monitor, + monitor->get_pixel_scale() * default_timeline_width / 4, + monitor->get_pixel_scale() * default_timeline_height / 4), + WinStatsGraph(monitor) +{ + _left_margin = _pixel_scale * 24; + _right_margin = _pixel_scale * 2; + _top_margin = _pixel_scale * 5; + _bottom_margin = _pixel_scale * 2; + + normal_guide_bars(); + + create_window(); + clear_region(); + + _grid_brush = CreateSolidBrush(RGB(0xdd, 0xdd, 0xdd)); +} + +/** + * + */ +WinStatsTimeline:: +~WinStatsTimeline() { +} + +/** + * Called as each frame's data is made available. There is no guarantee the + * frames will arrive in order, or that all of them will arrive at all. The + * monitor should be prepared to accept frames received out-of-order or + * missing. + */ +void WinStatsTimeline:: +new_data(int thread_index, int frame_number) { + PStatTimeline::new_data(thread_index, frame_number); +} + +/** + * Called when it is necessary to redraw the entire graph. + */ +void WinStatsTimeline:: +force_redraw() { + PStatTimeline::force_redraw(); +} + +/** + * Called when the user has resized the window, forcing a resize of the graph. + */ +void WinStatsTimeline:: +changed_graph_size(int graph_xsize, int graph_ysize) { + PStatTimeline::changed_size(graph_xsize, graph_ysize); +} + +/** + * Erases the chart area. + */ +void WinStatsTimeline:: +clear_region() { + RECT rect = { 0, 0, get_xsize(), get_ysize() }; + FillRect(_bitmap_dc, &rect, (HBRUSH)GetStockObject(WHITE_BRUSH)); +} + +/** + * Erases the chart area in preparation for drawing a bunch of bars. + */ +void WinStatsTimeline:: +begin_draw() { + SelectObject(_bitmap_dc, WinStatsGraph::_monitor->get_font()); + SelectObject(_bitmap_dc, GetStockObject(NULL_PEN)); + SetBkMode(_bitmap_dc, TRANSPARENT); + SetTextAlign(_bitmap_dc, TA_LEFT | TA_TOP | TA_NOUPDATECP); +} + +/** + * Draws a horizontal separator. + */ +void WinStatsTimeline:: +draw_separator(int row) { + int y = (row_to_pixel(row) + row_to_pixel(row + 1)) / 2; + RECT rect = {0, y, get_xsize(), y + _pixel_scale / 3}; + FillRect(_bitmap_dc, &rect, _grid_brush); +} + +/** + * Draws a vertical guide bar. If the row is -1, draws it in all rows. + */ +void WinStatsTimeline:: +draw_guide_bar(int x, GuideBarStyle style) { + int x1 = x - _pixel_scale / 6; + int x2 = x1 + _pixel_scale / 3; + if (style == GBS_frame) { + ++x2; + } + RECT rect = {x1, 0, x2, get_ysize()}; + FillRect(_bitmap_dc, &rect, _grid_brush); +} + +/** + * Draws a single bar in the chart for the indicated row, in the color for the + * given collector, for the indicated horizontal pixel range. + */ +void WinStatsTimeline:: +draw_bar(int row, int from_x, int to_x, int collector_index, + const std::string &collector_name) { + + int top = row_to_pixel(row); + int bottom = row_to_pixel(row + 1); + + bool is_highlighted = row == _highlighted_row && _highlighted_x >= from_x && _highlighted_x < to_x; + HBRUSH brush = get_collector_brush(collector_index, is_highlighted); + + if (to_x < from_x + 2) { + // It's just a tiny sliver. This is a more reliable way to draw it. + RECT rect = {from_x, top + 1, from_x + 1, bottom - 1}; + FillRect(_bitmap_dc, &rect, brush); + + //if (to_x <= from_x + 2) { + // // Draw an arrow pointing to it, if it's so small. + // POINT vertices[] = {{to_x, bottom}, {to_x - _pixel_scale, bottom + _pixel_scale * 2}, {to_x + _pixel_scale, bottom + _pixel_scale * 2}}; + // Polygon(_bitmap_dc, vertices, 3); + //} + } + else { + SelectObject(_bitmap_dc, brush); + RoundRect(_bitmap_dc, + std::max(from_x, -_pixel_scale - 1), + top, + std::min(std::max(to_x, from_x + 1), get_xsize() + _pixel_scale), + bottom, + _pixel_scale, + _pixel_scale); + + if ((to_x - from_x) >= _pixel_scale * 4) { + // Only bother drawing the text if we've got some space to draw on. + // Choose a suitable foreground color. + SetTextColor(_bitmap_dc, get_collector_text_color(collector_index, is_highlighted)); + + // Make sure that the text doesn't run off the chart. + SIZE size; + GetTextExtentPoint32(_bitmap_dc, collector_name.data(), collector_name.size(), &size); + int center = (from_x + to_x) / 2; + int left = std::max(from_x, 0) + _pixel_scale / 2; + int right = std::min(to_x, get_xsize()) - _pixel_scale / 2; + + if (size.cx >= right - left) { + if (right - left < _pixel_scale * 6) { + // It's a really tiny space. Draw a single letter. + RECT rect = {left, top, right, bottom}; + DrawText(_bitmap_dc, collector_name.data(), 1, + &rect, DT_CENTER | DT_SINGLELINE | DT_VCENTER); + } else { + // It's going to be tricky to fit it, let Windows figure it out via + // the more expensive DrawText call. + RECT rect = {left, top, right, bottom}; + DrawText(_bitmap_dc, collector_name.data(), collector_name.size(), + &rect, DT_CENTER | DT_END_ELLIPSIS | DT_SINGLELINE | DT_VCENTER); + } + } + else { + int text_top = top + (bottom - top - size.cy) / 2; + if (center - size.cx / 2 < 0) { + // Put it against the left-most edge. + TextOut(_bitmap_dc, _pixel_scale, text_top, + collector_name.data(), collector_name.length()); + } + else if (center + size.cx / 2 >= get_xsize()) { + // Put it against the right-most edge. + TextOut(_bitmap_dc, get_xsize() - _pixel_scale - size.cx, text_top, + collector_name.data(), collector_name.length()); + } + else { + // It fits just fine, center it. + TextOut(_bitmap_dc, center - size.cx / 2, text_top, + collector_name.data(), collector_name.length()); + } + } + } + } +} + +/** + * Called after all the bars have been drawn, this triggers a refresh event to + * draw it to the window. + */ +void WinStatsTimeline:: +end_draw() { + InvalidateRect(_graph_window, nullptr, FALSE); + + if (_threads_changed) { + RECT rect; + GetClientRect(_window, &rect); + rect.top = _top_margin; + rect.right = _left_margin; + InvalidateRect(_window, &rect, TRUE); + _threads_changed = false; + } + + if (_guide_bars_changed) { + RECT rect; + GetClientRect(_window, &rect); + rect.bottom = _top_margin; + InvalidateRect(_window, &rect, TRUE); + _guide_bars_changed = false; + } +} + +/** + * Called at the end of the draw cycle. + */ +void WinStatsTimeline:: +idle() { +} + +/** + * Overridden by a derived class to implement an animation. If it returns + * false, the animation timer is stopped. + */ +bool WinStatsTimeline:: +animate(double time, double dt) { + return PStatTimeline::animate(time, dt); +} + +/** + * + */ +LONG WinStatsTimeline:: +window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { + switch (msg) { + case WM_MOUSELEAVE: + SetFocus(nullptr); + break; + + default: + break; + } + + return WinStatsGraph::window_proc(hwnd, msg, wparam, lparam); +} + +/** + * + */ +LONG WinStatsTimeline:: +graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { + switch (msg) { + case WM_LBUTTONDOWN: + if (_potential_drag_mode == DM_none) { + set_drag_mode(DM_pan); + int16_t x = LOWORD(lparam); + _drag_start_x = x; + _scroll_speed = 0.0; + _zoom_center = pixel_to_timestamp(x); + SetCapture(_graph_window); + return 0; + } + break; + + case WM_MOUSEMOVE: + // Make sure we can accept keyboard events, except if we're inactive. + if (GetActiveWindow() == _window) { + SetFocus(hwnd); + } + + if (_drag_mode == DM_none && _potential_drag_mode == DM_none) { + // When the mouse is over a color bar, highlight it. + int x = LOWORD(lparam); + int y = HIWORD(lparam); + double time = pixel_to_timestamp(x); + + int row = pixel_to_row(y); + + if (row != _highlighted_row) { + clear_graph_tooltip(); + } + else if (_highlighted_row >= 0) { + // Is the mouse on the same bar? If not, clear the tooltip. + ColorBar bar; + if (find_bar(row, x, bar)) { + double prev_time = pixel_to_timestamp(_highlighted_x); + if (prev_time < bar._start || prev_time > bar._end) { + clear_graph_tooltip(); + } + } else { + clear_graph_tooltip(); + } + } + + std::swap(_highlighted_x, x); + std::swap(_highlighted_row, row); + + if (row >= 0) { + PStatTimeline::force_redraw(row, x, x); + } + PStatTimeline::force_redraw(_highlighted_row, _highlighted_x, _highlighted_x); + + if ((_keys_held & (F_w | F_s)) != 0) { + // Update the zoom center if we move the mouse while zooming with the + // keyboard. + _zoom_center = time; + } + + // Now we want to get a WM_MOUSELEAVE when the mouse leaves the graph + // window. + TRACKMOUSEEVENT tme = { + sizeof(TRACKMOUSEEVENT), + TME_LEAVE, + _graph_window, + 0 + }; + TrackMouseEvent(&tme); + } + else { + // If the mouse is in some drag mode, stop highlighting. + if (_highlighted_row != -1) { + int row = _highlighted_row; + _highlighted_row = -1; + PStatTimeline::force_redraw(row, _highlighted_x, _highlighted_x); + clear_graph_tooltip(); + } + } + + if (_drag_mode == DM_pan) { + int16_t x = LOWORD(lparam); + int delta = _drag_start_x - x; + set_horizontal_scroll(get_horizontal_scroll() + pixel_to_height(delta)); + _drag_start_x = x; + return 0; + } + break; + + case WM_MOUSELEAVE: + // When the mouse leaves the graph, stop highlighting. + if (_highlighted_row != -1) { + int row = _highlighted_row; + _highlighted_row = -1; + PStatTimeline::force_redraw(row, _highlighted_x, _highlighted_x); + clear_graph_tooltip(); + } + SetFocus(nullptr); + break; + + case WM_LBUTTONUP: + if (_drag_mode == DM_pan) { + set_drag_mode(DM_none); + ReleaseCapture(); + return 0; + } + break; + + case WM_LBUTTONDBLCLK: + { + // Double-clicking on a color bar in the graph will zoom the graph into + // that collector. + int16_t x = LOWORD(lparam); + int16_t y = HIWORD(lparam); + int row = pixel_to_row(y); + ColorBar bar; + if (find_bar(row, x, bar)) { + double width = bar._end - bar._start; + zoom_to(width * 1.5, pixel_to_timestamp(x)); + scroll_to(bar._start - width / 4.0); + } else { + // Double-clicking the white area zooms out. + _zoom_speed -= 100.0; + } + start_animation(); + return 0; + } + break; + + case WM_CONTEXTMENU: + { + // Right-clicking a color bar brings up a context menu. + POINT point; + if (GetCursorPos(&point)) { + POINT graph_point = point; + if (ScreenToClient(_graph_window, &graph_point)) { + int row = pixel_to_row(graph_point.y); + ColorBar bar; + if (find_bar(row, graph_point.x, bar)) { + _popup_bar = bar; + + HMENU popup = CreatePopupMenu(); + + std::string label = get_bar_tooltip(row, graph_point.x); + if (!label.empty()) { + AppendMenu(popup, MF_STRING | MF_DISABLED, 0, label.c_str()); + } + AppendMenu(popup, MF_STRING, 101, "Zoom To"); + AppendMenu(popup, MF_STRING, 102, "Open Strip Chart"); + AppendMenu(popup, MF_STRING, 103, "Open Flame Graph"); + AppendMenu(popup, MF_STRING, 104, "Open Piano Roll"); + TrackPopupMenu(popup, TPM_LEFTBUTTON, point.x, point.y, 0, _graph_window, nullptr); + } + } + } + return 0; + } + break; + + case WM_COMMAND: + switch (LOWORD(wparam)) { + case 101: + { + double width = _popup_bar._end - _popup_bar._start; + zoom_to(width * 1.5, (_popup_bar._end + _popup_bar._start) / 2.0); + scroll_to(_popup_bar._start - width / 4.0); + start_animation(); + } + return 0; + + case 102: + WinStatsGraph::_monitor->open_strip_chart(_popup_bar._thread_index, _popup_bar._collector_index, false); + return 0; + + case 103: + WinStatsGraph::_monitor->open_flame_graph(_popup_bar._thread_index, _popup_bar._collector_index); + return 0; + + case 104: + WinStatsGraph::_monitor->open_piano_roll(_popup_bar._thread_index); + return 0; + } + break; + + case WM_MOUSEWHEEL: + { + if (GET_KEYSTATE_WPARAM(wparam) & MK_CONTROL) { + // Zoom in/out around the cursor position. + POINT point; + if (GetCursorPos(&point) && ScreenToClient(_graph_window, &point)) { + int delta = GET_WHEEL_DELTA_WPARAM(wparam); + zoom_by(delta / 120.0, pixel_to_timestamp(point.x)); + start_animation(); + } + } + return 0; + } + break; + + case WM_MOUSEHWHEEL: + { + int delta = GET_WHEEL_DELTA_WPARAM(wparam); + _scroll_speed += delta / 12.0; + start_animation(); + return 0; + } + break; + + case WM_KEYDOWN: + case WM_KEYUP: + { + int flag = 0; + int vsc = (lparam & 0xff0000) >> 16; + if ((lparam & 0x1000000) == 0) { + // Accept WASD based on their position rather than their mapping + switch (vsc) { + case 17: + flag = F_w; + break; + case 30: + flag = F_a; + break; + case 31: + flag = F_s; + break; + case 32: + flag = F_d; + break; + } + } + if (flag == 0) { + switch (wparam) { + case VK_LEFT: + flag = F_left; + break; + case VK_RIGHT: + flag = F_right; + break; + case 'W': + flag = F_w; + break; + case 'A': + flag = F_a; + break; + case 'S': + flag = F_s; + break; + case 'D': + flag = F_d; + break; + } + } + if (flag != 0) { + if (msg == WM_KEYDOWN) { + if (flag & (F_w | F_s)) { + POINT point; + if (GetCursorPos(&point) && ScreenToClient(_graph_window, &point)) { + _zoom_center = pixel_to_timestamp(point.x); + } else { + _zoom_center = get_horizontal_scroll() + get_horizontal_scale() / 2.0; + } + } + if (_keys_held == 0) { + start_animation(); + } + _keys_held |= flag; + } + else if (_keys_held != 0) { + _keys_held &= ~flag; + } + } + } + break; + + case WM_KILLFOCUS: + _keys_held = 0; + break; + + default: + break; + } + + return WinStatsGraph::graph_window_proc(hwnd, msg, wparam, lparam); +} + +/** + * This is called during the servicing of WM_PAINT; it gives a derived class + * opportunity to do some further painting into the window (the outer window, + * not the graph window). + */ +void WinStatsTimeline:: +additional_window_paint(HDC hdc) { + // Draw in the labels for the guide bars. + SelectObject(hdc, WinStatsGraph::_monitor->get_font()); + SetTextAlign(hdc, TA_LEFT | TA_BOTTOM); + SetBkMode(hdc, TRANSPARENT); + + int y = _top_margin - 2; + + int num_guide_bars = get_num_guide_bars(); + for (int i = 0; i < num_guide_bars; ++i) { + draw_guide_label(hdc, y, get_guide_bar(i)); + } + + SetTextColor(hdc, _dark_color); + SetTextAlign(hdc, TA_LEFT | TA_TOP | TA_NOUPDATECP); + + for (const ThreadRow &thread_row : _threads) { + draw_thread_label(hdc, thread_row); + } +} + +/** + * This is called during the servicing of WM_PAINT; it gives a derived class + * opportunity to do some further painting into the window (the outer window, + * not the graph window). + */ +void WinStatsTimeline:: +additional_graph_window_paint(HDC hdc) { +} + +/** + * Called when the mouse hovers over the graph, and should return the text that + * should appear on the tooltip. + */ +std::string WinStatsTimeline:: +get_graph_tooltip(int mouse_x, int mouse_y) const { + return PStatTimeline::get_bar_tooltip(pixel_to_row(mouse_y), mouse_x); +} + +/** + * Based on the mouse position within the window's client area, look for + * draggable things the mouse might be hovering over and return the + * apprioprate DragMode enum or DM_none if nothing is indicated. + */ +WinStatsGraph::DragMode WinStatsTimeline:: +consider_drag_start(int mouse_x, int mouse_y, int width, int height) { + DragMode mode = WinStatsGraph::consider_drag_start(mouse_x, mouse_y, width, height); + if (mode == DM_right_margin) { + mode = DM_none; + } + return mode; +} + +/** + * Draws the text for the indicated guide bar label at the top of the graph. + */ +void WinStatsTimeline:: +draw_guide_label(HDC hdc, int y, const PStatGraph::GuideBar &bar) { + const std::string &label = bar._label; + if (label.empty()) { + return; + } + + switch (bar._style) { + case GBS_target: + SetTextColor(hdc, _light_color); + break; + + case GBS_user: + SetTextColor(hdc, _user_guide_bar_color); + break; + + case GBS_normal: + SetTextColor(hdc, _light_color); + break; + + case GBS_frame: + SetTextColor(hdc, _dark_color); + break; + } + + int x = timestamp_to_pixel(bar._height); + SIZE size; + GetTextExtentPoint32(hdc, label.data(), label.length(), &size); + + int this_x = _graph_left + x - size.cx / 2; + if (x >= 0 && x < get_xsize()) { + TextOut(hdc, this_x, y, + label.data(), label.length()); + } +} + +/** + * Draws the text for the indicated thread on the side of the graph. + */ +void WinStatsTimeline:: +draw_thread_label(HDC hdc, const ThreadRow &thread_row) { + int top = row_to_pixel(thread_row._row_offset + 1); + int bottom = row_to_pixel(thread_row._row_offset + 2); + + RECT rect = {_pixel_scale * 2, top, _left_margin - _pixel_scale * 2, bottom}; + DrawText(hdc, thread_row._label.data(), thread_row._label.size(), + &rect, DT_RIGHT | DT_END_ELLIPSIS | DT_SINGLELINE | DT_VCENTER); +} + +/** + * Creates the window for this strip chart. + */ +void WinStatsTimeline:: +create_window() { + if (_window) { + return; + } + + HINSTANCE application = GetModuleHandle(nullptr); + register_window_class(application); + + POINT window_pos = WinStatsGraph::_monitor->get_new_window_pos(); + + RECT win_rect = { + 0, 0, + _left_margin + get_xsize() + _right_margin, + _top_margin + get_ysize() + _bottom_margin + }; + + // compute window size based on desired client area size + AdjustWindowRect(&win_rect, graph_window_style, FALSE); + + _window = + CreateWindowEx(WS_EX_DLGMODALFRAME, _window_class_name, + "Timeline", graph_window_style, + window_pos.x, window_pos.y, + win_rect.right - win_rect.left, + win_rect.bottom - win_rect.top, + WinStatsGraph::_monitor->get_window(), nullptr, application, 0); + if (!_window) { + nout << "Could not create timeline window!\n"; + exit(1); + } + + SetWindowLongPtr(_window, 0, (LONG_PTR)this); + + // Ensure that the window is on top of the stack. + SetWindowPos(_window, HWND_TOP, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + + SetFocus(_window); +} + +/** + * Registers the window class for the Timeline window, if it has not already + * been registered. + */ +void WinStatsTimeline:: +register_window_class(HINSTANCE application) { + if (_window_class_registered) { + return; + } + + WNDCLASS wc; + + ZeroMemory(&wc, sizeof(WNDCLASS)); + wc.style = 0; + wc.lpfnWndProc = (WNDPROC)static_window_proc; + wc.hInstance = application; + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.hbrBackground = (HBRUSH)COLOR_WINDOW; + wc.lpszMenuName = nullptr; + wc.lpszClassName = _window_class_name; + + // Reserve space to associate the this pointer with the window. + wc.cbWndExtra = sizeof(WinStatsTimeline *); + + if (!RegisterClass(&wc)) { + nout << "Could not register Timeline window class!\n"; + exit(1); + } + + _window_class_registered = true; +} + +/** + * + */ +LONG WINAPI WinStatsTimeline:: +static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { + WinStatsTimeline *self = (WinStatsTimeline *)GetWindowLongPtr(hwnd, 0); + if (self != nullptr && self->_window == hwnd) { + return self->window_proc(hwnd, msg, wparam, lparam); + } else { + return DefWindowProc(hwnd, msg, wparam, lparam); + } +} diff --git a/pandatool/src/win-stats/winStatsTimeline.h b/pandatool/src/win-stats/winStatsTimeline.h new file mode 100644 index 0000000000..61dc07b943 --- /dev/null +++ b/pandatool/src/win-stats/winStatsTimeline.h @@ -0,0 +1,89 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file winStatsTimeline.h + * @author rdb + * @date 2022-02-11 + */ + +#ifndef WINSTATSTIMELINE_H +#define WINSTATSTIMELINE_H + +#include "pandatoolbase.h" + +#include "winStatsGraph.h" +#include "pStatTimeline.h" + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN 1 +#endif +#include + +class WinStatsMonitor; + +/** + * A window that draws all of the start/stop event pairs on each thread on a + * horizontal scrolling timeline, with concurrent start/stop pairs stacked + * underneath each other. + */ +class WinStatsTimeline : public PStatTimeline, public WinStatsGraph { +public: + WinStatsTimeline(WinStatsMonitor *monitor); + virtual ~WinStatsTimeline(); + + virtual void new_data(int thread_index, int frame_number); + virtual void force_redraw(); + virtual void changed_graph_size(int graph_xsize, int graph_ysize); + +protected: + virtual void clear_region(); + virtual void begin_draw(); + virtual void draw_separator(int row); + virtual void draw_guide_bar(int x, GuideBarStyle style); + virtual void draw_bar(int row, int from_x, int to_x, int collector_index, + const std::string &collector_name); + virtual void end_draw(); + virtual void idle(); + + virtual bool animate(double time, double dt); + + LONG window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); + virtual LONG graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); + virtual void additional_window_paint(HDC hdc); + virtual void additional_graph_window_paint(HDC hdc); + virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const; + virtual DragMode consider_drag_start(int mouse_x, int mouse_y, + int width, int height); + +private: + void draw_guide_label(HDC hdc, int y, const GuideBar &bar); + void draw_thread_label(HDC hdc, const ThreadRow &thread_row); + + void create_window(); + static void register_window_class(HINSTANCE application); + + static LONG WINAPI static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); + + int row_to_pixel(int y) const { + return y * _pixel_scale * 5 + _pixel_scale; + } + int pixel_to_row(int y) const { + return (y - _pixel_scale) / (_pixel_scale * 5); + } + + static bool _window_class_registered; + static const char * const _window_class_name; + + HBRUSH _grid_brush; + + int _highlighted_row = -1; + int _highlighted_x = 0; + ColorBar _popup_bar; +}; + +#endif diff --git a/pandatool/src/win-stats/winstats_composite1.cxx b/pandatool/src/win-stats/winstats_composite1.cxx index cc90367573..ef63280222 100644 --- a/pandatool/src/win-stats/winstats_composite1.cxx +++ b/pandatool/src/win-stats/winstats_composite1.cxx @@ -6,5 +6,6 @@ #include "winStatsLabelStack.cxx" #include "winStatsMonitor.cxx" #include "winStatsPianoRoll.cxx" +#include "winStatsTimeline.cxx" #include "winStatsServer.cxx" #include "winStatsStripChart.cxx"