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:
rdb 2022-02-15 17:49:46 +01:00
parent c0c5eeb27e
commit 161ac4c2f7
53 changed files with 5310 additions and 741 deletions

View File

@ -26,6 +26,7 @@ set(GTKSTATS_SOURCES
gtkStatsPianoRoll.cxx
gtkStatsServer.cxx
gtkStatsStripChart.cxx
gtkStatsTimeline.cxx
)
composite_sources(gtk-stats GTKSTATS_SOURCES)

View File

@ -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.
*/

View File

@ -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;

View File

@ -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();
}
}
/**

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -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);

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
};

View File

@ -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,

View File

@ -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);

View File

@ -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,

View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -8,3 +8,4 @@
#include "gtkStatsPianoRoll.cxx"
#include "gtkStatsServer.cxx"
#include "gtkStatsStripChart.cxx"
#include "gtkStatsTimeline.cxx"

View File

@ -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)

View File

@ -8,5 +8,6 @@
#include "pStatServer.cxx"
#include "pStatStripChart.cxx"
#include "pStatThreadData.cxx"
#include "pStatTimeline.cxx"
#include "pStatView.cxx"
#include "pStatViewLevel.cxx"

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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";
}
}

View File

@ -52,6 +52,7 @@ public:
GBS_normal,
GBS_target,
GBS_user,
GBS_frame,
};
class GuideBar {

View File

@ -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.

View File

@ -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

View File

@ -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();

View File

@ -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.
*/

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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) {

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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

View File

@ -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);

View File

@ -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)

View File

@ -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);
}
}
}

View File

@ -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.

View File

@ -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;

View File

@ -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);
}
}
/**

View File

@ -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();

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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;
};

View File

@ -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);

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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

View File

@ -6,5 +6,6 @@
#include "winStatsLabelStack.cxx"
#include "winStatsMonitor.cxx"
#include "winStatsPianoRoll.cxx"
#include "winStatsTimeline.cxx"
#include "winStatsServer.cxx"
#include "winStatsStripChart.cxx"