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
This commit is contained in:
parent
c0c5eeb27e
commit
161ac4c2f7
|
|
@ -26,6 +26,7 @@ set(GTKSTATS_SOURCES
|
|||
gtkStatsPianoRoll.cxx
|
||||
gtkStatsServer.cxx
|
||||
gtkStatsStripChart.cxx
|
||||
gtkStatsTimeline.cxx
|
||||
)
|
||||
|
||||
composite_sources(gtk-stats GTKSTATS_SOURCES)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<int, GtkStatsLabel *> _labels;
|
||||
|
||||
GtkWidget *_top_hbox;
|
||||
GtkWidget *_average_check_box;
|
||||
GtkWidget *_total_label;
|
||||
GtkWidget *_fixed;
|
||||
|
||||
int _popup_index = -1;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
#include "pandatoolbase.h"
|
||||
#include "gtkStatsLabelStack.h"
|
||||
#include "pmap.h"
|
||||
#include "luse.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include <cairo.h>
|
||||
|
|
@ -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<int, std::pair<cairo_pattern_t *, cairo_pattern_t *> > Brushes;
|
||||
Brushes _brushes;
|
||||
|
||||
typedef pmap<int, std::pair<LRGBColor, LRGBColor> > 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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<int> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<GtkStatsGraph *> 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<int> _status_bar_collectors;
|
||||
pvector<GtkWidget *> _status_bar_labels;
|
||||
std::string _window_title;
|
||||
int _time_units;
|
||||
double _scroll_speed;
|
||||
bool _pause;
|
||||
double _resolution;
|
||||
|
||||
friend class GtkStatsGraph;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -8,3 +8,4 @@
|
|||
#include "gtkStatsPianoRoll.cxx"
|
||||
#include "gtkStatsServer.cxx"
|
||||
#include "gtkStatsStripChart.cxx"
|
||||
#include "gtkStatsTimeline.cxx"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -8,5 +8,6 @@
|
|||
#include "pStatServer.cxx"
|
||||
#include "pStatStripChart.cxx"
|
||||
#include "pStatThreadData.cxx"
|
||||
#include "pStatTimeline.cxx"
|
||||
#include "pStatView.cxx"
|
||||
#include "pStatViewLevel.cxx"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<int, CollectorData> 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<int, StackLevel> _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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ public:
|
|||
GBS_normal,
|
||||
GBS_target,
|
||||
GBS_user,
|
||||
GBS_frame,
|
||||
};
|
||||
|
||||
class GuideBar {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<double> 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 <algorithm>
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
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<int collector_index, double start_time>
|
||||
pvector<std::pair<int, double> > 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;
|
||||
}
|
||||
|
|
@ -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<ColorBar> Row;
|
||||
typedef pvector<Row> 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<ThreadRow> 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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@
|
|||
|
||||
#include <commctrl.h>
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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<int, WinStatsLabel *> _labels;
|
||||
|
||||
HWND _average_check_box;
|
||||
int _popup_index = -1;
|
||||
|
||||
static bool _window_class_registered;
|
||||
static const char * const _window_class_name;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
#include "winStatsGraph.h"
|
||||
#include "winStatsMonitor.h"
|
||||
#include "winStatsLabelStack.h"
|
||||
#include "trueClock.h"
|
||||
#include "convert_srgb.h"
|
||||
|
||||
#include <commctrl.h>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<int, std::pair<HBRUSH, HBRUSH> > Brushes;
|
||||
Brushes _brushes;
|
||||
|
||||
typedef pmap<int, std::pair<COLORREF, COLORREF> > 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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <algorithm>
|
||||
|
||||
#include <commctrl.h>
|
||||
|
||||
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<std::string> parts;
|
||||
pvector<int> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<int> _status_bar_collectors;
|
||||
std::string _window_title;
|
||||
int _time_units;
|
||||
double _scroll_speed;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@
|
|||
|
||||
#include <commctrl.h>
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <windows.h>
|
||||
|
||||
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
|
||||
|
|
@ -6,5 +6,6 @@
|
|||
#include "winStatsLabelStack.cxx"
|
||||
#include "winStatsMonitor.cxx"
|
||||
#include "winStatsPianoRoll.cxx"
|
||||
#include "winStatsTimeline.cxx"
|
||||
#include "winStatsServer.cxx"
|
||||
#include "winStatsStripChart.cxx"
|
||||
|
|
|
|||
Loading…
Reference in New Issue