2
0
Эх сурвалжийг харах

pstatserver: Major improvements to PStats server UI, including:

* New "Flame Graph" chart for seeing all collectors in a frame, much easier to read than piano roll
* Update controls, fonts, background color to more modern visual style on Windows
* Proper support for high DPI monitors (with correct scaling)
* Add tooltips for collector labels showing full name and averaged value
* Colors of collectors are now converted to sRGB transfer encoding
* Major performance improvement to piano roll view on Windows
* Movering mouse over labels now highlights the corresponding area in chart
* Label hover effect changed to darkening effect instead of border
* Reimplement graph as static common control on Windows
* Check boxes are now clickable by their label on Windows
* Graph windows have minimum sizes on Windows
rdb 3 жил өмнө
parent
commit
7da70cf939
44 өөрчлөгдсөн 2807 нэмэгдсэн , 413 устгасан
  1. 1 1
      makepanda/makepanda.py
  2. 2 0
      pandatool/src/gtk-stats/CMakeLists.txt
  3. 28 10
      pandatool/src/gtk-stats/gtkStatsChartMenu.cxx
  4. 551 0
      pandatool/src/gtk-stats/gtkStatsFlameGraph.cxx
  5. 81 0
      pandatool/src/gtk-stats/gtkStatsFlameGraph.h
  6. 62 23
      pandatool/src/gtk-stats/gtkStatsGraph.cxx
  7. 11 13
      pandatool/src/gtk-stats/gtkStatsGraph.h
  8. 88 33
      pandatool/src/gtk-stats/gtkStatsLabel.cxx
  9. 13 10
      pandatool/src/gtk-stats/gtkStatsLabel.h
  10. 1 3
      pandatool/src/gtk-stats/gtkStatsLabelStack.cxx
  11. 14 0
      pandatool/src/gtk-stats/gtkStatsMonitor.cxx
  12. 1 0
      pandatool/src/gtk-stats/gtkStatsMonitor.h
  13. 21 8
      pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx
  14. 2 1
      pandatool/src/gtk-stats/gtkStatsPianoRoll.h
  15. 29 21
      pandatool/src/gtk-stats/gtkStatsStripChart.cxx
  16. 2 1
      pandatool/src/gtk-stats/gtkStatsStripChart.h
  17. 1 0
      pandatool/src/gtk-stats/gtkstats_composite1.cxx
  18. 4 1
      pandatool/src/pstatserver/CMakeLists.txt
  19. 1 0
      pandatool/src/pstatserver/p3pstatserver_composite1.cxx
  20. 89 0
      pandatool/src/pstatserver/pStatFlameGraph.I
  21. 297 0
      pandatool/src/pstatserver/pStatFlameGraph.cxx
  22. 107 0
      pandatool/src/pstatserver/pStatFlameGraph.h
  23. 9 0
      pandatool/src/pstatserver/pStatStripChart.I
  24. 64 5
      pandatool/src/pstatserver/pStatStripChart.cxx
  25. 3 1
      pandatool/src/pstatserver/pStatStripChart.h
  26. 4 2
      pandatool/src/win-stats/CMakeLists.txt
  27. 14 0
      pandatool/src/win-stats/winStats.cxx
  28. 21 8
      pandatool/src/win-stats/winStatsChartMenu.cxx
  29. 639 0
      pandatool/src/win-stats/winStatsFlameGraph.cxx
  30. 80 0
      pandatool/src/win-stats/winStatsFlameGraph.h
  31. 107 84
      pandatool/src/win-stats/winStatsGraph.cxx
  32. 13 9
      pandatool/src/win-stats/winStatsGraph.h
  33. 68 0
      pandatool/src/win-stats/winStatsLabel.I
  34. 108 90
      pandatool/src/win-stats/winStatsLabel.cxx
  35. 18 10
      pandatool/src/win-stats/winStatsLabel.h
  36. 85 2
      pandatool/src/win-stats/winStatsLabelStack.cxx
  37. 4 0
      pandatool/src/win-stats/winStatsLabelStack.h
  38. 56 3
      pandatool/src/win-stats/winStatsMonitor.cxx
  39. 7 0
      pandatool/src/win-stats/winStatsMonitor.h
  40. 47 30
      pandatool/src/win-stats/winStatsPianoRoll.cxx
  41. 3 1
      pandatool/src/win-stats/winStatsPianoRoll.h
  42. 48 41
      pandatool/src/win-stats/winStatsStripChart.cxx
  43. 2 2
      pandatool/src/win-stats/winStatsStripChart.h
  44. 1 0
      pandatool/src/win-stats/winstats_composite1.cxx

+ 1 - 1
makepanda/makepanda.py

@@ -5848,7 +5848,7 @@ if not PkgSkip("PANDATOOL") and (GetTarget() == 'windows' or not PkgSkip("GTK3")
     TargetAdd('pstats.exe', input='libp3progbase.lib')
     TargetAdd('pstats.exe', input='libp3progbase.lib')
     TargetAdd('pstats.exe', input='libp3pandatoolbase.lib')
     TargetAdd('pstats.exe', input='libp3pandatoolbase.lib')
     TargetAdd('pstats.exe', input=COMMON_PANDA_LIBS)
     TargetAdd('pstats.exe', input=COMMON_PANDA_LIBS)
-    TargetAdd('pstats.exe', opts=['SUBSYSTEM:WINDOWS', 'WINSOCK', 'WINIMM', 'WINGDI', 'WINKERNEL', 'WINOLDNAMES', 'WINUSER', 'WINMM', 'GTK3'])
+    TargetAdd('pstats.exe', opts=['SUBSYSTEM:WINDOWS', 'WINCOMCTL', 'WINSOCK', 'WINIMM', 'WINGDI', 'WINKERNEL', 'WINOLDNAMES', 'WINUSER', 'WINMM', 'GTK3'])
 
 
 #
 #
 # DIRECTORY: pandatool/src/xfileprogs/
 # DIRECTORY: pandatool/src/xfileprogs/

+ 2 - 0
pandatool/src/gtk-stats/CMakeLists.txt

@@ -4,6 +4,7 @@ endif()
 
 
 set(GTKSTATS_HEADERS
 set(GTKSTATS_HEADERS
   gtkStatsChartMenu.h
   gtkStatsChartMenu.h
+  gtkStatsFlameGraph.h
   gtkStatsGraph.h
   gtkStatsGraph.h
   gtkStatsLabel.h
   gtkStatsLabel.h
   gtkStatsLabelStack.h
   gtkStatsLabelStack.h
@@ -17,6 +18,7 @@ set(GTKSTATS_HEADERS
 set(GTKSTATS_SOURCES
 set(GTKSTATS_SOURCES
   gtkStats.cxx
   gtkStats.cxx
   gtkStatsChartMenu.cxx
   gtkStatsChartMenu.cxx
+  gtkStatsFlameGraph.cxx
   gtkStatsGraph.cxx
   gtkStatsGraph.cxx
   gtkStatsLabel.cxx
   gtkStatsLabel.cxx
   gtkStatsLabelStack.cxx
   gtkStatsLabelStack.cxx

+ 28 - 10
pandatool/src/gtk-stats/gtkStatsChartMenu.cxx

@@ -116,20 +116,34 @@ do_update() {
     }
     }
   }
   }
 
 
-  // Also a menu item for a piano roll (following a separator).
+  // Also menu items for flame graph and piano roll (following a separator).
   GtkWidget *sep = gtk_separator_menu_item_new();
   GtkWidget *sep = gtk_separator_menu_item_new();
   gtk_widget_show(sep);
   gtk_widget_show(sep);
   gtk_menu_shell_append(GTK_MENU_SHELL(_menu), sep);
   gtk_menu_shell_append(GTK_MENU_SHELL(_menu), sep);
 
 
-  GtkStatsMonitor::MenuDef smd(_thread_index, -1, false);
-  const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd);
+  {
+    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("Piano Roll");
-  gtk_widget_show(menu_item);
-  gtk_menu_shell_append(GTK_MENU_SHELL(_menu), menu_item);
+    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);
+    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);
+    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);
+  }
 }
 }
 
 
 /**
 /**
@@ -189,9 +203,13 @@ handle_menu(gpointer data) {
     return;
     return;
   }
   }
 
 
-  if (menu_def->_collector_index < 0) {
+  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);
     monitor->open_piano_roll(menu_def->_thread_index);
-  } else {
+  }
+  else {
     monitor->open_strip_chart(menu_def->_thread_index,
     monitor->open_strip_chart(menu_def->_thread_index,
             menu_def->_collector_index,
             menu_def->_collector_index,
             menu_def->_show_level);
             menu_def->_show_level);

+ 551 - 0
pandatool/src/gtk-stats/gtkStatsFlameGraph.cxx

@@ -0,0 +1,551 @@
+/**
+ * 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 gtkStatsFlameGraph.cxx
+ * @author rdb
+ * @date 2022-02-02
+ */
+
+#include "gtkStatsFlameGraph.h"
+#include "gtkStatsLabel.h"
+#include "gtkStatsMonitor.h"
+#include "pStatCollectorDef.h"
+
+static const int default_flame_graph_width = 800;
+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)
+{
+  // Let's show the units on the guide bar labels.  There's room.
+  set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
+
+  // 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);
+
+  _average_check_box = gtk_check_button_new_with_label("Average");
+  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(_average_check_box), TRUE);
+  g_signal_connect(G_OBJECT(_average_check_box), "toggled",
+       G_CALLBACK(toggled_callback), this);
+
+  // 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(draw_callback), this);
+
+  _total_label = gtk_label_new("");
+  gtk_box_pack_start(GTK_BOX(_top_hbox), _average_check_box, FALSE, FALSE, 0);
+  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_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(_window, 0, 0);
+
+  clear_region();
+}
+
+/**
+ *
+ */
+GtkStatsFlameGraph::
+~GtkStatsFlameGraph() {
+}
+
+/**
+ * Called whenever a new Collector definition is received from the client.
+ */
+void GtkStatsFlameGraph::
+new_collector(int collector_index) {
+  GtkStatsGraph::new_collector(collector_index);
+}
+
+/**
+ * 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 GtkStatsFlameGraph::
+new_data(int thread_index, int frame_number) {
+  if (is_title_unknown()) {
+    std::string window_title = get_title_text();
+    if (!is_title_unknown()) {
+      gtk_window_set_title(GTK_WINDOW(_window), window_title.c_str());
+    }
+  }
+
+  if (!_pause) {
+    update();
+
+    std::string text = format_number(get_horizontal_scale(), get_guide_bar_units(), get_guide_bar_unit_name());
+    if (_net_value_text != text) {
+      _net_value_text = text;
+      gtk_label_set_text(GTK_LABEL(_total_label), _net_value_text.c_str());
+    }
+  }
+}
+
+/**
+ * Called when it is necessary to redraw the entire graph.
+ */
+void GtkStatsFlameGraph::
+force_redraw() {
+  PStatFlameGraph::force_redraw();
+}
+
+/**
+ * Called when the user has resized the window, forcing a resize of the graph.
+ */
+void GtkStatsFlameGraph::
+changed_graph_size(int graph_xsize, int graph_ysize) {
+  PStatFlameGraph::changed_size(graph_xsize, graph_ysize);
+}
+
+/**
+ * Called when the user selects a new time units from the monitor pulldown
+ * menu, this should adjust the units for the graph to the indicated mask if
+ * it is a time-based graph.
+ */
+void GtkStatsFlameGraph::
+set_time_units(int unit_mask) {
+  int old_unit_mask = get_guide_bar_units();
+  if ((old_unit_mask & (GBU_hz | GBU_ms)) != 0) {
+    unit_mask = unit_mask & (GBU_hz | GBU_ms);
+    unit_mask |= (old_unit_mask & GBU_show_units);
+    set_guide_bar_units(unit_mask);
+
+    gtk_widget_queue_draw(_scale_area);
+  }
+}
+
+/**
+ * Called when the user single-clicks on a label.
+ */
+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);
+    }
+  }
+}
+
+/**
+ * Called when the user hovers the mouse over a label.
+ */
+void GtkStatsFlameGraph::
+on_enter_label(int collector_index) {
+  if (collector_index != _highlighted_index) {
+    _highlighted_index = collector_index;
+  }
+}
+
+/**
+ * Called when the user's mouse cursor leaves a label.
+ */
+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;
+    }
+    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());
+}
+
+/**
+ * Calls update_guide_bars with parameters suitable to this kind of graph.
+ */
+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)));
+
+  _guide_bars.clear();
+
+  double dist = get_horizontal_scale() / num_bars;
+
+  for (int i = 1; i < num_bars; ++i) {
+    _guide_bars.push_back(make_guide_bar(i * dist));
+  }
+
+  _guide_bars_changed = true;
+}
+
+/**
+ * Erases the chart area.
+ */
+void GtkStatsFlameGraph::
+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 GtkStatsFlameGraph::
+begin_draw() {
+  clear_region();
+
+  // Draw in the guide bars.
+  int num_guide_bars = get_num_guide_bars();
+  for (int i = 0; i < num_guide_bars; i++) {
+    draw_guide_bar(_cr, get_guide_bar(i));
+  }
+}
+
+/**
+ * Called after all the bars have been drawn, this triggers a refresh event to
+ * draw it to the window.
+ */
+void GtkStatsFlameGraph::
+end_draw() {
+  gtk_widget_queue_draw(_graph_window);
+}
+
+/**
+ * Called at the end of the draw cycle.
+ */
+void GtkStatsFlameGraph::
+idle() {
+}
+
+/**
+ * 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 GtkStatsFlameGraph::
+additional_graph_window_paint(cairo_t *cr) {
+  int num_user_guide_bars = get_num_user_guide_bars();
+  for (int i = 0; i < num_user_guide_bars; i++) {
+    draw_guide_bar(cr, get_user_guide_bar(i));
+  }
+}
+
+/**
+ * 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.
+ */
+GtkStatsGraph::DragMode GtkStatsFlameGraph::
+consider_drag_start(int graph_x, int graph_y) {
+  if (graph_y >= 0 && graph_y < get_ysize()) {
+    if (graph_x >= 0 && graph_x < get_xsize()) {
+      // See if the mouse is over a user-defined guide bar.
+      int x = graph_x;
+      double from_height = pixel_to_height(x - 2);
+      double to_height = pixel_to_height(x + 2);
+      _drag_guide_bar = find_user_guide_bar(from_height, to_height);
+      if (_drag_guide_bar >= 0) {
+        return DM_guide_bar;
+      }
+
+    } else {
+      // The mouse is left or right of the graph; maybe create a new guide
+      // bar.
+      return DM_new_guide_bar;
+    }
+  }
+
+  return DM_none;
+}
+
+/**
+ * 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) {
+  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());
+      return TRUE;
+    }
+  }
+
+  if (_potential_drag_mode == DM_none) {
+    set_drag_mode(DM_scale);
+    _drag_scale_start = pixel_to_height(graph_x);
+    // SetCapture(_graph_window);
+    return TRUE;
+
+  } else if (_potential_drag_mode == DM_guide_bar && _drag_guide_bar >= 0) {
+    set_drag_mode(DM_guide_bar);
+    _drag_start_x = graph_x;
+    // SetCapture(_graph_window);
+    return TRUE;
+  }
+
+  return GtkStatsGraph::handle_button_press(widget, graph_x, graph_y,
+              double_click);
+}
+
+/**
+ * Called when the mouse button is released within the graph window.
+ */
+gboolean GtkStatsFlameGraph::
+handle_button_release(GtkWidget *widget, 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) {
+    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(widget, graph_x, graph_y);
+  }
+
+  return GtkStatsGraph::handle_button_release(widget, 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) {
+  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()) {
+      set_drag_mode(DM_guide_bar);
+      _drag_guide_bar = add_user_guide_bar(pixel_to_height(graph_x));
+      return TRUE;
+    }
+  }
+  else if (_drag_mode == DM_guide_bar) {
+    move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_x));
+    return TRUE;
+  }
+
+  return GtkStatsGraph::handle_motion(widget, graph_x, graph_y);
+}
+
+/**
+ * Draws the line for the indicated guide bar on the graph.
+ */
+void GtkStatsFlameGraph::
+draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar) {
+  int x = height_to_pixel(bar._height);
+
+  if (x > 0 && x < get_xsize() - 1) {
+    // Only draw it if it's not too close to the top.
+    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_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
+      break;
+    }
+    cairo_move_to(cr, x, 0);
+    cairo_line_to(cr, x, get_ysize());
+    cairo_stroke(cr);
+  }
+}
+
+/**
+ * This is called during the servicing of the draw event.
+ */
+void GtkStatsFlameGraph::
+draw_guide_labels(cairo_t *cr) {
+  int i;
+  int num_guide_bars = get_num_guide_bars();
+  for (i = 0; i < num_guide_bars; i++) {
+    draw_guide_label(cr, get_guide_bar(i));
+  }
+
+  int num_user_guide_bars = get_num_user_guide_bars();
+  for (i = 0; i < num_user_guide_bars; i++) {
+    draw_guide_label(cr, get_user_guide_bar(i));
+  }
+}
+
+/**
+ * Draws the text for the indicated guide bar label at the top of the graph.
+ */
+void GtkStatsFlameGraph::
+draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) {
+  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_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
+    break;
+  }
+
+  int x = height_to_pixel(bar._height);
+  const std::string &label = bar._label;
+
+  PangoLayout *layout = gtk_widget_create_pango_layout(_window, 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);
+    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);
+      return;
+    }
+  }
+
+  if (x >= 0 && x < get_xsize()) {
+    // Now convert our x to a coordinate within our drawing area.
+    int junk_y;
+
+    // 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);
+}
+
+/**
+ * Called when the average check box is toggled.
+ */
+void GtkStatsFlameGraph::
+toggled_callback(GtkToggleButton *button, gpointer data) {
+  GtkStatsFlameGraph *self = (GtkStatsFlameGraph *)data;
+
+  bool active = gtk_toggle_button_get_active(button);
+  self->set_average_mode(active);
+}
+
+/**
+ * Draws in the scale labels.
+ */
+gboolean GtkStatsFlameGraph::
+draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) {
+  GtkStatsFlameGraph *self = (GtkStatsFlameGraph *)data;
+  self->draw_guide_labels(cr);
+
+  return TRUE;
+}

+ 81 - 0
pandatool/src/gtk-stats/gtkStatsFlameGraph.h

@@ -0,0 +1,81 @@
+/**
+ * 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 gtkStatsFlameGraph.h
+ * @author rdb
+ * @date 2022-02-02
+ */
+
+#ifndef GTKSTATSFLAMEGRAPH_H
+#define GTKSTATSFLAMEGRAPH_H
+
+#include "pandatoolbase.h"
+
+#include "gtkStatsGraph.h"
+#include "pStatFlameGraph.h"
+
+class GtkStatsLabel;
+
+/**
+ * A window that draws a flame chart, which shows the collectors explicitly
+ * stopping and starting, one frame at a time.
+ */
+class GtkStatsFlameGraph : public PStatFlameGraph, public GtkStatsGraph {
+public:
+  GtkStatsFlameGraph(GtkStatsMonitor *monitor, int thread_index,
+                     int collector_index=0);
+  virtual ~GtkStatsFlameGraph();
+
+  virtual void new_collector(int collector_index);
+  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);
+
+  virtual void set_time_units(int unit_mask);
+  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 end_draw();
+  virtual void idle();
+
+  virtual void additional_graph_window_paint(cairo_t *cr);
+  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);
+
+private:
+  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);
+
+  static void toggled_callback(GtkToggleButton *button, gpointer data);
+  static gboolean draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data);
+
+private:
+  std::string _net_value_text;
+  pmap<int, GtkStatsLabel *> _labels;
+
+  GtkWidget *_top_hbox;
+  GtkWidget *_average_check_box;
+  GtkWidget *_total_label;
+  GtkWidget *_fixed;
+};
+
+#endif

+ 62 - 23
pandatool/src/gtk-stats/gtkStatsGraph.cxx

@@ -14,6 +14,7 @@
 #include "gtkStatsGraph.h"
 #include "gtkStatsGraph.h"
 #include "gtkStatsMonitor.h"
 #include "gtkStatsMonitor.h"
 #include "gtkStatsLabelStack.h"
 #include "gtkStatsLabelStack.h"
+#include "convert_srgb.h"
 
 
 const double GtkStatsGraph::rgb_light_gray[3] = {
 const double GtkStatsGraph::rgb_light_gray[3] = {
   0x9a / (double)0xff, 0x9a / (double)0xff, 0x9a / (double)0xff,
   0x9a / (double)0xff, 0x9a / (double)0xff, 0x9a / (double)0xff,
@@ -84,15 +85,20 @@ GtkStatsGraph(GtkStatsMonitor *monitor) :
   g_signal_connect(G_OBJECT(_graph_window), "motion_notify_event",
   g_signal_connect(G_OBJECT(_graph_window), "motion_notify_event",
        G_CALLBACK(motion_notify_event_callback), this);
        G_CALLBACK(motion_notify_event_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);
+
   // A Frame to hold the graph.
   // A Frame to hold the graph.
-  GtkWidget *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_window);
+  _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);
 
 
   // A VBox to hold the graph's frame, and any numbers (scale legend?  total?)
   // A VBox to hold the graph's frame, and any numbers (scale legend?  total?)
   // above it.
   // above it.
   _graph_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
   _graph_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
-  gtk_box_pack_end(GTK_BOX(_graph_vbox), graph_frame,
+  gtk_box_pack_end(GTK_BOX(_graph_vbox), _graph_frame,
        TRUE, TRUE, 0);
        TRUE, TRUE, 0);
 
 
   // An HBox to hold the graph's frame, and the scale legend to the right of
   // An HBox to hold the graph's frame, and the scale legend to the right of
@@ -103,10 +109,11 @@ GtkStatsGraph(GtkStatsMonitor *monitor) :
 
 
   // An HPaned to hold the label stack and the graph hbox.
   // An HPaned to hold the label stack and the graph hbox.
   _hpaned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
   _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_add(GTK_CONTAINER(_window), _hpaned);
   gtk_container_set_border_width(GTK_CONTAINER(_window), 8);
   gtk_container_set_border_width(GTK_CONTAINER(_window), 8);
 
 
-  gtk_paned_pack1(GTK_PANED(_hpaned), _label_stack.get_widget(), TRUE, TRUE);
+  gtk_paned_pack1(GTK_PANED(_hpaned), _label_stack.get_widget(), FALSE, FALSE);
   gtk_paned_pack2(GTK_PANED(_hpaned), _graph_hbox, TRUE, TRUE);
   gtk_paned_pack2(GTK_PANED(_hpaned), _graph_hbox, TRUE, TRUE);
 
 
   _drag_mode = DM_none;
   _drag_mode = DM_none;
@@ -124,10 +131,11 @@ GtkStatsGraph::
   _monitor = nullptr;
   _monitor = nullptr;
   release_surface();
   release_surface();
 
 
-  Brushes::iterator bi;
-  for (bi = _brushes.begin(); bi != _brushes.end(); ++bi) {
-    cairo_pattern_destroy((*bi).second);
+  for (auto &item : _brushes) {
+    cairo_pattern_destroy(item.second.first);
+    cairo_pattern_destroy(item.second.second);
   }
   }
+  _brushes.clear();
 
 
   _label_stack.clear_labels();
   _label_stack.clear_labels();
 
 
@@ -152,13 +160,6 @@ void GtkStatsGraph::
 new_data(int thread_index, int frame_number) {
 new_data(int thread_index, int frame_number) {
 }
 }
 
 
-/**
- * Called when it is necessary to redraw the entire graph.
- */
-void GtkStatsGraph::
-force_redraw() {
-}
-
 /**
 /**
  * Called when the user has resized the window, forcing a resize of the graph.
  * Called when the user has resized the window, forcing a resize of the graph.
  */
  */
@@ -207,7 +208,38 @@ user_guide_bars_changed() {
  * Called when the user single-clicks on a label.
  * Called when the user single-clicks on a label.
  */
  */
 void GtkStatsGraph::
 void GtkStatsGraph::
-clicked_label(int collector_index) {
+on_click_label(int collector_index) {
+}
+
+/**
+ * Called when the user hovers the mouse over a label.
+ */
+void GtkStatsGraph::
+on_enter_label(int collector_index) {
+  if (collector_index != _highlighted_index) {
+    _highlighted_index = collector_index;
+    force_redraw();
+  }
+}
+
+/**
+ * Called when the user's mouse cursor leaves a label.
+ */
+void GtkStatsGraph::
+on_leave_label(int collector_index) {
+  if (collector_index == _highlighted_index && collector_index != -1) {
+    _highlighted_index = -1;
+    force_redraw();
+  }
+}
+
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string GtkStatsGraph::
+get_label_tooltip(int collector_index) const {
+  return std::string();
 }
 }
 
 
 /**
 /**
@@ -232,23 +264,30 @@ close() {
  * Returns a pattern suitable for drawing in the indicated collector's color.
  * Returns a pattern suitable for drawing in the indicated collector's color.
  */
  */
 cairo_pattern_t *GtkStatsGraph::
 cairo_pattern_t *GtkStatsGraph::
-get_collector_pattern(int collector_index) {
+get_collector_pattern(int collector_index, bool highlight) {
   Brushes::iterator bi;
   Brushes::iterator bi;
   bi = _brushes.find(collector_index);
   bi = _brushes.find(collector_index);
   if (bi != _brushes.end()) {
   if (bi != _brushes.end()) {
-    return (*bi).second;
+    return highlight ? (*bi).second.second : (*bi).second.first;
   }
   }
 
 
   // Ask the monitor what color this guy should be.
   // Ask the monitor what color this guy should be.
   LRGBColor rgb = _monitor->get_collector_color(collector_index);
   LRGBColor rgb = _monitor->get_collector_color(collector_index);
-  cairo_pattern_t *pattern = cairo_pattern_create_rgb(rgb[0], rgb[1], rgb[2]);
-
-  _brushes[collector_index] = pattern;
-  return pattern;
+  cairo_pattern_t *pattern = cairo_pattern_create_rgb(
+    encode_sRGB_float(rgb[0]),
+    encode_sRGB_float(rgb[1]),
+    encode_sRGB_float(rgb[2]));
+  cairo_pattern_t *hpattern = cairo_pattern_create_rgb(
+    encode_sRGB_float(rgb[0] * 0.75f),
+    encode_sRGB_float(rgb[1] * 0.75f),
+    encode_sRGB_float(rgb[2] * 0.75f));
+
+  _brushes[collector_index] = std::make_pair(pattern, hpattern);
+  return highlight ? hpattern : pattern;
 }
 }
 
 
 /**
 /**
- * This is called during the servicing of expose_event; it gives a derived
+ * 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.
  * class opportunity to do some further painting into the graph window.
  */
  */
 void GtkStatsGraph::
 void GtkStatsGraph::

+ 11 - 13
pandatool/src/gtk-stats/gtkStatsGraph.h

@@ -44,7 +44,7 @@ public:
 
 
   virtual void new_collector(int collector_index);
   virtual void new_collector(int collector_index);
   virtual void new_data(int thread_index, int frame_number);
   virtual void new_data(int thread_index, int frame_number);
-  virtual void force_redraw();
+  virtual void force_redraw()=0;
   virtual void changed_graph_size(int graph_xsize, int graph_ysize);
   virtual void changed_graph_size(int graph_xsize, int graph_ysize);
 
 
   virtual void set_time_units(int unit_mask);
   virtual void set_time_units(int unit_mask);
@@ -52,11 +52,14 @@ public:
   void set_pause(bool pause);
   void set_pause(bool pause);
 
 
   void user_guide_bars_changed();
   void user_guide_bars_changed();
-  virtual void clicked_label(int collector_index);
+  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:
 protected:
   void close();
   void close();
-  cairo_pattern_t *get_collector_pattern(int collector_index);
+  cairo_pattern_t *get_collector_pattern(int collector_index, bool highlight = false);
 
 
   virtual void additional_graph_window_paint(cairo_t *cr);
   virtual void additional_graph_window_paint(cairo_t *cr);
   virtual DragMode consider_drag_start(int graph_x, int graph_y);
   virtual DragMode consider_drag_start(int graph_x, int graph_y);
@@ -69,12 +72,14 @@ protected:
 
 
 protected:
 protected:
   // Table of patterns for our various collectors.
   // Table of patterns for our various collectors.
-  typedef pmap<int, cairo_pattern_t *> Brushes;
+  typedef pmap<int, std::pair<cairo_pattern_t *, cairo_pattern_t *> > Brushes;
   Brushes _brushes;
   Brushes _brushes;
 
 
   GtkStatsMonitor *_monitor;
   GtkStatsMonitor *_monitor;
   GtkWidget *_parent_window;
   GtkWidget *_parent_window;
   GtkWidget *_window;
   GtkWidget *_window;
+  GtkWidget *_graph_frame;
+  GtkWidget *_graph_overlay;
   GtkWidget *_graph_window;
   GtkWidget *_graph_window;
   GtkWidget *_graph_hbox;
   GtkWidget *_graph_hbox;
   GtkWidget *_graph_vbox;
   GtkWidget *_graph_vbox;
@@ -88,21 +93,14 @@ protected:
   cairo_t *_cr;
   cairo_t *_cr;
   int _surface_xsize, _surface_ysize;
   int _surface_xsize, _surface_ysize;
 
 
-  /*
-  COLORREF _dark_color;
-  COLORREF _light_color;
-  COLORREF _user_guide_bar_color;
-  HPEN _dark_pen;
-  HPEN _light_pen;
-  HPEN _user_guide_bar_pen;
-  */
-
   DragMode _drag_mode;
   DragMode _drag_mode;
   DragMode _potential_drag_mode;
   DragMode _potential_drag_mode;
   int _drag_start_x, _drag_start_y;
   int _drag_start_x, _drag_start_y;
   double _drag_scale_start;
   double _drag_scale_start;
   int _drag_guide_bar;
   int _drag_guide_bar;
 
 
+  int _highlighted_index = -1;
+
   bool _pause;
   bool _pause;
 
 
   static const double rgb_white[3];
   static const double rgb_white[3];

+ 88 - 33
pandatool/src/gtk-stats/gtkStatsLabel.cxx

@@ -14,6 +14,7 @@
 #include "gtkStatsLabel.h"
 #include "gtkStatsLabel.h"
 #include "gtkStatsMonitor.h"
 #include "gtkStatsMonitor.h"
 #include "gtkStatsGraph.h"
 #include "gtkStatsGraph.h"
+#include "convert_srgb.h"
 
 
 int GtkStatsLabel::_left_margin = 2;
 int GtkStatsLabel::_left_margin = 2;
 int GtkStatsLabel::_right_margin = 2;
 int GtkStatsLabel::_right_margin = 2;
@@ -25,19 +26,14 @@ int GtkStatsLabel::_bottom_margin = 2;
  */
  */
 GtkStatsLabel::
 GtkStatsLabel::
 GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph,
 GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph,
-              int thread_index, int collector_index, bool use_fullname) :
+              int thread_index, int collector_index, bool use_fullname,
+              bool align_right) :
   _monitor(monitor),
   _monitor(monitor),
   _graph(graph),
   _graph(graph),
   _thread_index(thread_index),
   _thread_index(thread_index),
-  _collector_index(collector_index)
+  _collector_index(collector_index),
+  _align_right(align_right)
 {
 {
-  _widget = nullptr;
-  if (use_fullname) {
-    _text = _monitor->get_client_data()->get_collector_fullname(_collector_index);
-  } else {
-    _text = _monitor->get_client_data()->get_collector_name(_collector_index);
-  }
-
   _widget = gtk_drawing_area_new();
   _widget = gtk_drawing_area_new();
   gtk_widget_add_events(_widget,
   gtk_widget_add_events(_widget,
       GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK |
       GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK |
@@ -50,32 +46,41 @@ GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph,
        G_CALLBACK(leave_notify_event_callback), this);
        G_CALLBACK(leave_notify_event_callback), this);
   g_signal_connect(G_OBJECT(_widget), "button_press_event",
   g_signal_connect(G_OBJECT(_widget), "button_press_event",
        G_CALLBACK(button_press_event_callback), this);
        G_CALLBACK(button_press_event_callback), this);
+  g_signal_connect(G_OBJECT(_widget), "query-tooltip",
+       G_CALLBACK(query_tooltip_callback), this);
 
 
+  gtk_widget_set_has_tooltip(_widget, TRUE);
   gtk_widget_show(_widget);
   gtk_widget_show(_widget);
 
 
-  // Make up a PangoLayout to represent the text.
-  _layout = gtk_widget_create_pango_layout(_widget, _text.c_str());
-
   // Set the fg and bg colors on the label.
   // Set the fg and bg colors on the label.
-  _bg_color = _monitor->get_collector_color(_collector_index);
+  LRGBColor rgb = _monitor->get_collector_color(_collector_index);
+  _bg_color = LRGBColor(
+    encode_sRGB_float(rgb[0]),
+    encode_sRGB_float(rgb[1]),
+    encode_sRGB_float(rgb[2]));
+
+  _highlight_bg_color = LRGBColor(
+    encode_sRGB_float(rgb[0] * 0.75f),
+    encode_sRGB_float(rgb[1] * 0.75f),
+    encode_sRGB_float(rgb[2] * 0.75f));
 
 
   // Should our foreground be black or white?
   // Should our foreground be black or white?
-  PN_stdfloat bright = _bg_color.dot(LRGBColor(0.299, 0.587, 0.114));
+  PN_stdfloat bright = _bg_color.dot(LRGBColor(0.2126, 0.7152, 0.0722));
   if (bright >= 0.5) {
   if (bright >= 0.5) {
     _fg_color = LRGBColor(0);
     _fg_color = LRGBColor(0);
   } else {
   } else {
     _fg_color = LRGBColor(1);
     _fg_color = LRGBColor(1);
   }
   }
-
-  // What are the extents of the text?  This determines the minimum size of
-  // our widget.
-  int width, height;
-  pango_layout_get_pixel_size(_layout, &width, &height);
-  gtk_widget_set_size_request(_widget, width + 8, height);
+  if (bright >= 0.5 * 0.75) {
+    _highlight_fg_color = LRGBColor(0);
+  } else {
+    _highlight_fg_color = LRGBColor(1);
+  }
 
 
   _highlight = false;
   _highlight = false;
   _mouse_within = false;
   _mouse_within = false;
-  _height = height;
+
+  update_text(use_fullname);
 }
 }
 
 
 /**
 /**
@@ -83,7 +88,10 @@ GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph,
  */
  */
 GtkStatsLabel::
 GtkStatsLabel::
 ~GtkStatsLabel() {
 ~GtkStatsLabel() {
-  // DeleteObject(_bg_brush);
+  if (_layout) {
+    g_object_unref(_layout);
+    _layout = nullptr;
+  }
 }
 }
 
 
 /**
 /**
@@ -137,6 +145,33 @@ get_highlight() const {
   return _highlight;
   return _highlight;
 }
 }
 
 
+/**
+ * Set to true if the full name of the collector should be shown.
+ */
+void GtkStatsLabel::
+update_text(bool use_fullname) {
+  const PStatClientData *client_data = _monitor->get_client_data();
+  if (use_fullname) {
+    _text = client_data->get_collector_fullname(_collector_index);
+  } else {
+    _text = client_data->get_collector_name(_collector_index);
+  }
+
+  // Make up a PangoLayout to represent the text.
+  if (_layout) {
+    g_object_unref(_layout);
+  }
+  _layout = gtk_widget_create_pango_layout(_widget, _text.c_str());
+
+  // What are the extents of the text?  This determines the minimum size of
+  // our widget.
+  int width, height;
+  pango_layout_get_pixel_size(_layout, &width, &height);
+  gtk_widget_set_size_request(_widget, width + 8, height);
+  _ideal_width = width;
+  _height = height;
+}
+
 /**
 /**
  * Used internally to indicate whether the mouse is within the label's widget.
  * Used internally to indicate whether the mouse is within the label's widget.
  */
  */
@@ -155,7 +190,16 @@ gboolean GtkStatsLabel::
 draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) {
 draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) {
   GtkStatsLabel *self = (GtkStatsLabel *)data;
   GtkStatsLabel *self = (GtkStatsLabel *)data;
 
 
-  cairo_set_source_rgb(cr, self->_bg_color[0], self->_bg_color[1], self->_bg_color[2]);
+  LRGBColor bg, fg;
+  if (self->_highlight || self->_mouse_within) {
+    bg = self->_highlight_bg_color;
+    fg = self->_highlight_fg_color;
+  } else {
+    bg = self->_bg_color;
+    fg = self->_fg_color;
+
+  }
+  cairo_set_source_rgb(cr, bg[0], bg[1], bg[2]);
 
 
   GtkAllocation allocation;
   GtkAllocation allocation;
   gtk_widget_get_allocation(widget, &allocation);
   gtk_widget_get_allocation(widget, &allocation);
@@ -163,19 +207,16 @@ draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) {
   cairo_rectangle(cr, 0, 0, allocation.width, allocation.height);
   cairo_rectangle(cr, 0, 0, allocation.width, allocation.height);
   cairo_fill(cr);
   cairo_fill(cr);
 
 
-  // Center the text within the rectangle.
   int width, height;
   int width, height;
   pango_layout_get_pixel_size(self->_layout, &width, &height);
   pango_layout_get_pixel_size(self->_layout, &width, &height);
 
 
-  cairo_set_source_rgb(cr, self->_fg_color[0], self->_fg_color[1], self->_fg_color[2]);
-  cairo_move_to(cr, (allocation.width - width) / 2, 0);
-  pango_cairo_show_layout(cr, self->_layout);
-
-  // Now draw the highlight rectangle, if any.
-  if (self->_highlight || self->_mouse_within) {
-    cairo_rectangle(cr, 0, 0, allocation.width, allocation.height);
-    cairo_stroke(cr);
+  cairo_set_source_rgb(cr, fg[0], fg[1], fg[2]);
+  if (self->_align_right) {
+    cairo_move_to(cr, allocation.width - width, 0);
+  } else {
+    cairo_move_to(cr, 0, 0);
   }
   }
+  pango_cairo_show_layout(cr, self->_layout);
 
 
   return TRUE;
   return TRUE;
 }
 }
@@ -211,7 +252,21 @@ button_press_event_callback(GtkWidget *widget, GdkEventButton *event,
   GtkStatsLabel *self = (GtkStatsLabel *)data;
   GtkStatsLabel *self = (GtkStatsLabel *)data;
   bool double_click = (event->type == GDK_2BUTTON_PRESS);
   bool double_click = (event->type == GDK_2BUTTON_PRESS);
   if (double_click) {
   if (double_click) {
-    self->_graph->clicked_label(self->_collector_index);
+    self->_graph->on_click_label(self->_collector_index);
   }
   }
   return TRUE;
   return TRUE;
 }
 }
+
+/**
+ * Called when a tooltip should be displayed.
+ */
+gboolean GtkStatsLabel::
+query_tooltip_callback(GtkWidget *widget, gint x, gint y,
+                gboolean keyboard_tip, GtkTooltip *tooltip,
+                gpointer data) {
+  GtkStatsLabel *self = (GtkStatsLabel *)data;
+
+  std::string text = self->_graph->get_label_tooltip(self->_collector_index);
+  gtk_tooltip_set_text(tooltip, text.c_str());
+  return !text.empty();
+}

+ 13 - 10
pandatool/src/gtk-stats/gtkStatsLabel.h

@@ -30,7 +30,8 @@ class GtkStatsGraph;
 class GtkStatsLabel {
 class GtkStatsLabel {
 public:
 public:
   GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph,
   GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph,
-                int thread_index, int collector_index, bool use_fullname);
+                int thread_index, int collector_index, bool use_fullname,
+                bool align_right = true);
   ~GtkStatsLabel();
   ~GtkStatsLabel();
 
 
   GtkWidget *get_widget() const;
   GtkWidget *get_widget() const;
@@ -42,6 +43,8 @@ public:
   void set_highlight(bool highlight);
   void set_highlight(bool highlight);
   bool get_highlight() const;
   bool get_highlight() const;
 
 
+  void update_text(bool use_fullname);
+
 private:
 private:
   void set_mouse_within(bool mouse_within);
   void set_mouse_within(bool mouse_within);
   static gboolean draw_callback(GtkWidget *widget,
   static gboolean draw_callback(GtkWidget *widget,
@@ -55,6 +58,9 @@ private:
   static gboolean button_press_event_callback(GtkWidget *widget,
   static gboolean button_press_event_callback(GtkWidget *widget,
                 GdkEventButton *event,
                 GdkEventButton *event,
                 gpointer data);
                 gpointer data);
+  static gboolean query_tooltip_callback(GtkWidget *widget, gint x, gint y,
+                gboolean keyboard_tip, GtkTooltip *tooltip,
+                gpointer data);
 
 
   GtkStatsMonitor *_monitor;
   GtkStatsMonitor *_monitor;
   GtkStatsGraph *_graph;
   GtkStatsGraph *_graph;
@@ -63,19 +69,16 @@ private:
   std::string _text;
   std::string _text;
   GtkWidget *_widget;
   GtkWidget *_widget;
   LRGBColor _fg_color;
   LRGBColor _fg_color;
+  LRGBColor _highlight_fg_color;
   LRGBColor _bg_color;
   LRGBColor _bg_color;
-  PangoLayout *_layout;
-
-  /*
-  COLORREF _bg_color;
-  COLORREF _fg_color;
-  HBRUSH _bg_brush;
-  HBRUSH _highlight_brush;
-  */
+  LRGBColor _highlight_bg_color;
+  PangoLayout *_layout = nullptr;
 
 
+  int _height;
+  int _ideal_width;
   bool _highlight;
   bool _highlight;
   bool _mouse_within;
   bool _mouse_within;
-  int _height;
+  bool _align_right;
 
 
   static int _left_margin, _right_margin;
   static int _left_margin, _right_margin;
   static int _top_margin, _bottom_margin;
   static int _top_margin, _bottom_margin;

+ 1 - 3
pandatool/src/gtk-stats/gtkStatsLabelStack.cxx

@@ -84,9 +84,7 @@ get_label_collector_index(int label_index) const {
  */
  */
 void GtkStatsLabelStack::
 void GtkStatsLabelStack::
 clear_labels(bool delete_widgets) {
 clear_labels(bool delete_widgets) {
-  Labels::iterator li;
-  for (li = _labels.begin(); li != _labels.end(); ++li) {
-    GtkStatsLabel *label = (*li);
+  for (GtkStatsLabel *label : _labels) {
     if (delete_widgets) {
     if (delete_widgets) {
       gtk_container_remove(GTK_CONTAINER(_widget), label->get_widget());
       gtk_container_remove(GTK_CONTAINER(_widget), label->get_widget());
     }
     }

+ 14 - 0
pandatool/src/gtk-stats/gtkStatsMonitor.cxx

@@ -17,6 +17,7 @@
 #include "gtkStatsStripChart.h"
 #include "gtkStatsStripChart.h"
 #include "gtkStatsChartMenu.h"
 #include "gtkStatsChartMenu.h"
 #include "gtkStatsPianoRoll.h"
 #include "gtkStatsPianoRoll.h"
+#include "gtkStatsFlameGraph.h"
 #include "gtkStatsMenuId.h"
 #include "gtkStatsMenuId.h"
 #include "pStatGraph.h"
 #include "pStatGraph.h"
 #include "pStatCollectorDef.h"
 #include "pStatCollectorDef.h"
@@ -251,6 +252,19 @@ open_piano_roll(int thread_index) {
   graph->set_pause(_pause);
   graph->set_pause(_pause);
 }
 }
 
 
+/**
+ * Opens a new flame graph showing the indicated data.
+ */
+void GtkStatsMonitor::
+open_flame_graph(int thread_index) {
+  GtkStatsFlameGraph *graph = new GtkStatsFlameGraph(this, thread_index);
+  add_graph(graph);
+
+  graph->set_time_units(_time_units);
+  graph->set_scroll_speed(_scroll_speed);
+  graph->set_pause(_pause);
+}
+
 /**
 /**
  * Adds a new MenuDef to the monitor, or returns an existing one if there is
  * Adds a new MenuDef to the monitor, or returns an existing one if there is
  * already one just like it.
  * already one just like it.

+ 1 - 0
pandatool/src/gtk-stats/gtkStatsMonitor.h

@@ -66,6 +66,7 @@ public:
   GtkWidget *get_window() const;
   GtkWidget *get_window() const;
   void open_strip_chart(int thread_index, int collector_index, bool show_level);
   void open_strip_chart(int thread_index, int collector_index, bool show_level);
   void open_piano_roll(int thread_index);
   void open_piano_roll(int thread_index);
+  void open_flame_graph(int thread_index);
 
 
   const MenuDef *add_menu(const MenuDef &menu_def);
   const MenuDef *add_menu(const MenuDef &menu_def);
 
 

+ 21 - 8
pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx

@@ -16,7 +16,7 @@
 #include "numeric_types.h"
 #include "numeric_types.h"
 #include "gtkStatsLabelStack.h"
 #include "gtkStatsLabelStack.h"
 
 
-static const int default_piano_roll_width = 400;
+static const int default_piano_roll_width = 600;
 static const int default_piano_roll_height = 200;
 static const int default_piano_roll_height = 200;
 
 
 /**
 /**
@@ -126,7 +126,7 @@ set_time_units(int unit_mask) {
  * Called when the user single-clicks on a label.
  * Called when the user single-clicks on a label.
  */
  */
 void GtkStatsPianoRoll::
 void GtkStatsPianoRoll::
-clicked_label(int collector_index) {
+on_click_label(int collector_index) {
   if (collector_index >= 0) {
   if (collector_index >= 0) {
     GtkStatsGraph::_monitor->open_strip_chart(_thread_index, collector_index, false);
     GtkStatsGraph::_monitor->open_strip_chart(_thread_index, collector_index, false);
   }
   }
@@ -167,6 +167,18 @@ begin_draw() {
   }
   }
 }
 }
 
 
+/**
+ * Should be overridden by the user class.  This hook will be called before
+ * drawing any one row of bars.  These bars correspond to the collector whose
+ * index is get_row_collector(row), and in the color get_row_color(row).
+ */
+void GtkStatsPianoRoll::
+begin_row(int row) {
+  int collector_index = get_label_collector(row);
+  cairo_set_source(_cr, get_collector_pattern(collector_index,
+    _highlighted_index == collector_index));
+}
+
 /**
 /**
  * Draws a single bar on the chart.
  * Draws a single bar on the chart.
  */
  */
@@ -176,8 +188,6 @@ draw_bar(int row, int from_x, int to_x) {
     int y = _label_stack.get_label_y(row, _graph_window);
     int y = _label_stack.get_label_y(row, _graph_window);
     int height = _label_stack.get_label_height(row);
     int height = _label_stack.get_label_height(row);
 
 
-    int collector_index = get_label_collector(row);
-    cairo_set_source(_cr, get_collector_pattern(collector_index));
     cairo_rectangle(_cr, from_x, y - height + 2, to_x - from_x, height - 4);
     cairo_rectangle(_cr, from_x, y - height + 2, to_x - from_x, height - 4);
     cairo_fill(_cr);
     cairo_fill(_cr);
   }
   }
@@ -203,7 +213,7 @@ idle() {
 }
 }
 
 
 /**
 /**
- * This is called during the servicing of expose_event; it gives a derived
+ * 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.
  * class opportunity to do some further painting into the graph window.
  */
  */
 void GtkStatsPianoRoll::
 void GtkStatsPianoRoll::
@@ -251,7 +261,7 @@ handle_button_press(GtkWidget *widget, int graph_x, int graph_y,
   if (double_click) {
   if (double_click) {
     // Double-clicking on a color bar in the graph is the same as double-
     // Double-clicking on a color bar in the graph is the same as double-
     // clicking on the corresponding label.
     // clicking on the corresponding label.
-    clicked_label(get_collector_under_pixel(graph_x, graph_y));
+    on_click_label(get_collector_under_pixel(graph_x, graph_y));
     return TRUE;
     return TRUE;
   }
   }
 
 
@@ -303,7 +313,9 @@ gboolean GtkStatsPianoRoll::
 handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
 handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
   if (_drag_mode == DM_none && _potential_drag_mode == DM_none) {
   if (_drag_mode == DM_none && _potential_drag_mode == DM_none) {
     // When the mouse is over a color bar, highlight it.
     // When the mouse is over a color bar, highlight it.
-    _label_stack.highlight_label(get_collector_under_pixel(graph_x, graph_y));
+    int collector_index = get_collector_under_pixel(graph_x, graph_y);
+    _label_stack.highlight_label(collector_index);
+    on_enter_label(collector_index);
 
 
     /*
     /*
     // Now we want to get a WM_MOUSELEAVE when the mouse leaves the graph
     // Now we want to get a WM_MOUSELEAVE when the mouse leaves the graph
@@ -320,6 +332,7 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
   } else {
   } else {
     // If the mouse is in some drag mode, stop highlighting.
     // If the mouse is in some drag mode, stop highlighting.
     _label_stack.highlight_label(-1);
     _label_stack.highlight_label(-1);
+    on_leave_label(_highlighted_index);
   }
   }
 
 
   if (_drag_mode == DM_scale) {
   if (_drag_mode == DM_scale) {
@@ -409,7 +422,7 @@ draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar) {
 }
 }
 
 
 /**
 /**
- * This is called during the servicing of expose_event.
+ * This is called during the servicing of the draw event.
  */
  */
 void GtkStatsPianoRoll::
 void GtkStatsPianoRoll::
 draw_guide_labels(cairo_t *cr) {
 draw_guide_labels(cairo_t *cr) {

+ 2 - 1
pandatool/src/gtk-stats/gtkStatsPianoRoll.h

@@ -38,12 +38,13 @@ public:
   virtual void changed_graph_size(int graph_xsize, int graph_ysize);
   virtual void changed_graph_size(int graph_xsize, int graph_ysize);
 
 
   virtual void set_time_units(int unit_mask);
   virtual void set_time_units(int unit_mask);
-  virtual void clicked_label(int collector_index);
+  virtual void on_click_label(int collector_index);
   void set_horizontal_scale(double time_width);
   void set_horizontal_scale(double time_width);
 
 
 protected:
 protected:
   void clear_region();
   void clear_region();
   virtual void begin_draw();
   virtual void begin_draw();
+  virtual void begin_row(int row);
   virtual void draw_bar(int row, int from_x, int to_x);
   virtual void draw_bar(int row, int from_x, int to_x);
   virtual void end_draw();
   virtual void end_draw();
   virtual void idle();
   virtual void idle();

+ 29 - 21
pandatool/src/gtk-stats/gtkStatsStripChart.cxx

@@ -68,8 +68,15 @@ GtkStatsStripChart(GtkStatsMonitor *monitor, int thread_index,
        G_CALLBACK(draw_callback), this);
        G_CALLBACK(draw_callback), this);
   gtk_box_pack_start(GTK_BOX(_graph_hbox), _scale_area,
   gtk_box_pack_start(GTK_BOX(_graph_hbox), _scale_area,
          FALSE, FALSE, 0);
          FALSE, FALSE, 0);
-  gtk_widget_set_size_request(_scale_area, 40, 0);
 
 
+  // Make it wide enough to display a typical label.
+  {
+    PangoLayout *layout = gtk_widget_create_pango_layout(_window, "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,
   gtk_widget_set_size_request(_graph_window, default_strip_chart_width,
             default_strip_chart_height);
             default_strip_chart_height);
@@ -175,7 +182,7 @@ set_scroll_speed(double scroll_speed) {
  * Called when the user single-clicks on a label.
  * Called when the user single-clicks on a label.
  */
  */
 void GtkStatsStripChart::
 void GtkStatsStripChart::
-clicked_label(int collector_index) {
+on_click_label(int collector_index) {
   if (collector_index < 0) {
   if (collector_index < 0) {
     // Clicking on whitespace in the graph is the same as clicking on the top
     // Clicking on whitespace in the graph is the same as clicking on the top
     // label.
     // label.
@@ -202,6 +209,15 @@ clicked_label(int collector_index) {
   }
   }
 }
 }
 
 
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string GtkStatsStripChart::
+get_label_tooltip(int collector_index) const {
+  return PStatStripChart::get_label_tooltip(collector_index);
+}
+
 /**
 /**
  * Changes the value the height of the vertical axis represents.  This may
  * Changes the value the height of the vertical axis represents.  This may
  * force a redraw.
  * force a redraw.
@@ -287,7 +303,8 @@ draw_slice(int x, int w, const PStatStripChart::FrameData &fdata) {
   for (fi = fdata.begin(); fi != fdata.end(); ++fi) {
   for (fi = fdata.begin(); fi != fdata.end(); ++fi) {
     const ColorData &cd = (*fi);
     const ColorData &cd = (*fi);
     overall_time += cd._net_value;
     overall_time += cd._net_value;
-    cairo_set_source(_cr, get_collector_pattern(cd._collector_index));
+    cairo_set_source(_cr, get_collector_pattern(cd._collector_index,
+      _highlighted_index == cd._collector_index));
 
 
     if (overall_time > get_vertical_scale()) {
     if (overall_time > get_vertical_scale()) {
       // Off the top.  Go ahead and clamp it by hand, in case it's so far off
       // Off the top.  Go ahead and clamp it by hand, in case it's so far off
@@ -346,7 +363,7 @@ end_draw(int from_x, int to_x) {
 }
 }
 
 
 /**
 /**
- * This is called during the servicing of expose_event; it gives a derived
+ * 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.
  * class opportunity to do some further painting into the graph window.
  */
  */
 void GtkStatsStripChart::
 void GtkStatsStripChart::
@@ -418,7 +435,7 @@ handle_button_press(GtkWidget *widget, int graph_x, int graph_y,
     if (double_click) {
     if (double_click) {
       // Double-clicking on a color bar in the graph is the same as double-
       // Double-clicking on a color bar in the graph is the same as double-
       // clicking on the corresponding label.
       // clicking on the corresponding label.
-      clicked_label(get_collector_under_pixel(graph_x, graph_y));
+      on_click_label(get_collector_under_pixel(graph_x, graph_y));
       return TRUE;
       return TRUE;
     }
     }
 
 
@@ -473,23 +490,14 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
   if (_drag_mode == DM_none && _potential_drag_mode == DM_none &&
   if (_drag_mode == DM_none && _potential_drag_mode == DM_none &&
       graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
       graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
     // When the mouse is over a color bar, highlight it.
     // When the mouse is over a color bar, highlight it.
-    _label_stack.highlight_label(get_collector_under_pixel(graph_x, graph_y));
-
-    /*
-    // 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 {
+    int collector_index = get_collector_under_pixel(graph_x, graph_y);
+    _label_stack.highlight_label(collector_index);
+    on_enter_label(collector_index);
+  }
+  else {
     // If the mouse is in some drag mode, stop highlighting.
     // If the mouse is in some drag mode, stop highlighting.
     _label_stack.highlight_label(-1);
     _label_stack.highlight_label(-1);
+    on_leave_label(_highlighted_index);
   }
   }
 
 
   if (_drag_mode == DM_scale) {
   if (_drag_mode == DM_scale) {
@@ -546,7 +554,7 @@ draw_guide_bar(cairo_t *cr, int from_x, int to_x,
 }
 }
 
 
 /**
 /**
- * This is called during the servicing of expose_event.
+ * This is called during the servicing of the draw event.
  */
  */
 void GtkStatsStripChart::
 void GtkStatsStripChart::
 draw_guide_labels(cairo_t *cr) {
 draw_guide_labels(cairo_t *cr) {

+ 2 - 1
pandatool/src/gtk-stats/gtkStatsStripChart.h

@@ -40,7 +40,8 @@ public:
 
 
   virtual void set_time_units(int unit_mask);
   virtual void set_time_units(int unit_mask);
   virtual void set_scroll_speed(double scroll_speed);
   virtual void set_scroll_speed(double scroll_speed);
-  virtual void clicked_label(int collector_index);
+  virtual void on_click_label(int collector_index);
+  virtual std::string get_label_tooltip(int collector_index) const;
   void set_vertical_scale(double value_height);
   void set_vertical_scale(double value_height);
 
 
 protected:
 protected:

+ 1 - 0
pandatool/src/gtk-stats/gtkstats_composite1.cxx

@@ -1,5 +1,6 @@
 #include "gtkStats.cxx"
 #include "gtkStats.cxx"
 #include "gtkStatsChartMenu.cxx"
 #include "gtkStatsChartMenu.cxx"
+#include "gtkStatsFlameGraph.cxx"
 #include "gtkStatsGraph.cxx"
 #include "gtkStatsGraph.cxx"
 #include "gtkStatsLabel.cxx"
 #include "gtkStatsLabel.cxx"
 #include "gtkStatsLabelStack.cxx"
 #include "gtkStatsLabelStack.cxx"

+ 4 - 1
pandatool/src/pstatserver/CMakeLists.txt

@@ -4,6 +4,7 @@ endif()
 
 
 set(P3PSTATSERVER_HEADERS
 set(P3PSTATSERVER_HEADERS
   pStatClientData.h
   pStatClientData.h
+  pStatFlameGraph.h pStatFlameGraph.I
   pStatGraph.h pStatGraph.I
   pStatGraph.h pStatGraph.I
   pStatListener.h
   pStatListener.h
   pStatMonitor.h pStatMonitor.I
   pStatMonitor.h pStatMonitor.I
@@ -17,7 +18,9 @@ set(P3PSTATSERVER_HEADERS
 )
 )
 
 
 set(P3PSTATSERVER_SOURCES
 set(P3PSTATSERVER_SOURCES
-  pStatClientData.cxx pStatGraph.cxx
+  pStatClientData.cxx
+  pStatFlameGraph.cxx
+  pStatGraph.cxx
   pStatListener.cxx
   pStatListener.cxx
   pStatMonitor.cxx pStatPianoRoll.cxx
   pStatMonitor.cxx pStatPianoRoll.cxx
   pStatReader.cxx pStatServer.cxx
   pStatReader.cxx pStatServer.cxx

+ 1 - 0
pandatool/src/pstatserver/p3pstatserver_composite1.cxx

@@ -1,4 +1,5 @@
 #include "pStatClientData.cxx"
 #include "pStatClientData.cxx"
+#include "pStatFlameGraph.cxx"
 #include "pStatGraph.cxx"
 #include "pStatGraph.cxx"
 #include "pStatListener.cxx"
 #include "pStatListener.cxx"
 #include "pStatMonitor.cxx"
 #include "pStatMonitor.cxx"

+ 89 - 0
pandatool/src/pstatserver/pStatFlameGraph.I

@@ -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 pStatFlameGraph.I
+ * @author rdb
+ * @date 2022-01-28
+ */
+
+/**
+ * Returns the View this chart represents.
+ */
+INLINE PStatView &PStatFlameGraph::
+get_view() const {
+  return _view;
+}
+
+/**
+ * Returns the particular collector whose data this strip chart reflects.
+ */
+INLINE int PStatFlameGraph::
+get_collector_index() const {
+  return _collector_index;
+}
+
+/**
+ * Returns the amount of total time the width of the horizontal axis
+ * represents.
+ */
+INLINE double PStatFlameGraph::
+get_horizontal_scale() const {
+  return _time_width;
+}
+
+/**
+ * Changes the average_mode flag.  When true, the strip chart will average out
+ * 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.
+ */
+INLINE void PStatFlameGraph::
+set_average_mode(bool average_mode) {
+  if (_average_mode != average_mode) {
+    _average_mode = average_mode;
+    force_redraw();
+  }
+}
+
+/**
+ * Returns the current state of the average_mode flag.  When true, the strip
+ * chart will average out 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.
+ */
+INLINE bool PStatFlameGraph::
+get_average_mode() const {
+  return _average_mode;
+}
+
+/**
+ * Converts a value (i.e.  a "height" in the strip chart) to a horizontal
+ * pixel offset.
+ */
+INLINE int PStatFlameGraph::
+height_to_pixel(double value) const {
+  return (int)((double)_xsize * value / _time_width);
+}
+
+/**
+ * Converts a horizontal pixel offset to a value (a "height" in the strip
+ * chart).
+ */
+INLINE double PStatFlameGraph::
+pixel_to_height(int x) const {
+  return _time_width * (double)x / (double)_xsize;
+}
+
+/**
+ * Returns true if get_title_text() has never yet returned an answer, false if
+ * it has.
+ */
+INLINE bool PStatFlameGraph::
+is_title_unknown() const {
+  return _title_unknown;
+}

+ 297 - 0
pandatool/src/pstatserver/pStatFlameGraph.cxx

@@ -0,0 +1,297 @@
+/**
+ * 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 pStatFlameGraph.cxx
+ * @author rdb
+ * @date 2022-01-28
+ */
+
+#include "pStatFlameGraph.h"
+
+#include "pStatFrameData.h"
+#include "pStatCollectorDef.h"
+#include "string_utils.h"
+#include "config_pstatclient.h"
+
+#include <algorithm>
+#include <sstream>
+
+/**
+ *
+ */
+PStatFlameGraph::
+PStatFlameGraph(PStatMonitor *monitor, PStatView &view,
+                int thread_index, int collector_index, int xsize, int ysize) :
+  PStatGraph(monitor, xsize, ysize),
+  _thread_index(thread_index),
+  _view(view),
+  _collector_index(collector_index)
+{
+  _average_mode = true;
+  _average_cursor = 0;
+
+  _time_width = 1.0 / pstats_target_frame_rate;
+  _current_frame = -1;
+
+  _title_unknown = true;
+
+  _guide_bar_units = GBU_ms | GBU_hz | GBU_show_units;
+  normal_guide_bars();
+}
+
+/**
+ *
+ */
+PStatFlameGraph::
+~PStatFlameGraph() {
+}
+
+/**
+ * Updates the chart with the latest data.
+ */
+void PStatFlameGraph::
+update() {
+  const PStatClientData *client_data = _monitor->get_client_data();
+
+  // Don't bother to update the thread data until we know at least something
+  // about the collectors and threads.
+  if (client_data->get_num_collectors() != 0 &&
+      client_data->get_num_threads() != 0) {
+    const PStatThreadData *thread_data =
+      client_data->get_thread_data(_thread_index);
+    if (!thread_data->is_empty()) {
+      int frame_number = thread_data->get_latest_frame_number();
+      if (frame_number != _current_frame) {
+        _current_frame = frame_number;
+
+        update_data();
+        force_redraw();
+        update_labels();
+      }
+    }
+  }
+
+  idle();
+}
+
+/**
+ * Changes the collector represented by this flame graph.  This may force a
+ * redraw.
+ */
+void PStatFlameGraph::
+set_collector_index(int collector_index) {
+  if (_collector_index != collector_index) {
+    _collector_index = collector_index;
+    _title_unknown = true;
+    update_data();
+    force_redraw();
+    update_labels();
+  }
+}
+
+/**
+ * Returns the text suitable for the title label on the top line.
+ */
+std::string PStatFlameGraph::
+get_title_text() {
+  std::string 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)";
+    } else {
+      _title_unknown = true;
+    }
+  }
+
+  return text;
+}
+
+/**
+ * Called when the mouse hovers over a label, 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();
+  }
+
+  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()) << ")";
+  }
+
+  return text.str();
+}
+
+/**
+ *
+ */
+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;
+  }
+
+  _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;
+  }
+
+  if (cd._net_value != 0.0) {
+    cd._net_value = std::max(cd._net_value, 0.0);
+
+    double child_offset = offset;
+    offset += cd._net_value;
+
+    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);
+    }
+  }
+}
+
+/**
+ * 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 PStatFlameGraph::
+changed_size(int xsize, int ysize) {
+  if (xsize != _xsize || ysize != _ysize) {
+    _xsize = xsize;
+    _ysize = ysize;
+
+    normal_guide_bars();
+    force_redraw();
+    update_labels();
+  }
+}
+
+/**
+ * To be called by the user class when the whole thing needs to be redrawn for
+ * some reason.
+ */
+void PStatFlameGraph::
+force_redraw() {
+  begin_draw();
+  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.
+ */
+void PStatFlameGraph::
+normal_guide_bars() {
+  // We want vaguely 100 pixels between guide bars.
+  int num_bars = get_xsize() / 100;
+
+  _guide_bars.clear();
+
+  double dist = _time_width / num_bars;
+
+  for (int i = 1; i < num_bars; ++i) {
+    _guide_bars.push_back(make_guide_bar(i * dist));
+  }
+
+  _guide_bars_changed = true;
+}
+
+/**
+ * Should be overridden by the user class.  This hook will be called before
+ * drawing any bars in the chart.
+ */
+void PStatFlameGraph::
+begin_draw() {
+}
+
+/**
+ * Should be overridden by the user class.  This hook will be called after
+ * drawing a series of color bars in the chart.
+ */
+void PStatFlameGraph::
+end_draw() {
+}
+
+/**
+ * Should be overridden by the user class to perform any other updates might
+ * be necessary after the bars have been redrawn.
+ */
+void PStatFlameGraph::
+idle() {
+}

+ 107 - 0
pandatool/src/pstatserver/pStatFlameGraph.h

@@ -0,0 +1,107 @@
+/**
+ * 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 pStatFlameGraph.h
+ * @author rdb
+ * @date 2022-01-28
+ */
+
+#ifndef PSTATFLAMEGRAPH_H
+#define PSTATFLAMEGRAPH_H
+
+#include "pandatoolbase.h"
+
+#include "pStatGraph.h"
+#include "pStatMonitor.h"
+#include "pStatClientData.h"
+
+#include "pmap.h"
+#include "pdeque.h"
+
+class PStatFrameData;
+
+/**
+ * This is an abstract class that presents the interface for drawing a flame
+ * 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 manages all the flame chart logic; the actual nuts and bolts
+ * of drawing pixels is left to a user-derived class.
+ */
+class PStatFlameGraph : public PStatGraph {
+public:
+  PStatFlameGraph(PStatMonitor *monitor, PStatView &view,
+                  int thread_index, int collector_index,
+                  int xsize, int ysize);
+  virtual ~PStatFlameGraph();
+
+  void update();
+
+  INLINE PStatView &get_view() const;
+  INLINE int get_collector_index() const;
+  void set_collector_index(int collector_index);
+
+  INLINE double get_horizontal_scale() const;
+
+  INLINE void set_average_mode(bool average_mode);
+  INLINE bool get_average_mode() const;
+
+  INLINE int height_to_pixel(double value) const;
+  INLINE double pixel_to_height(int y) const;
+
+  INLINE bool is_title_unknown() const;
+  std::string get_title_text();
+  std::string get_label_tooltip(int collector_index) 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 end_draw();
+  virtual void idle();
+
+private:
+  void compute_page(const PStatFrameData &frame_data);
+
+protected:
+  int _thread_index;
+
+private:
+  PStatView &_view;
+  int _collector_index;
+  bool _average_mode;
+  size_t _average_cursor;
+
+  Data _data;
+
+  double _time_width;
+  int _current_frame;
+  bool _title_unknown;
+};
+
+#include "pStatFlameGraph.I"
+
+#endif

+ 9 - 0
pandatool/src/pstatserver/pStatStripChart.I

@@ -156,6 +156,15 @@ pixel_to_height(int x) const {
   return _value_height * (double)(get_ysize() - x) / (double)get_ysize();
   return _value_height * (double)(get_ysize() - x) / (double)get_ysize();
 }
 }
 
 
+/**
+ * Returns true if get_title_text() has never yet returned an answer, false if
+ * it has.
+ */
+INLINE bool PStatStripChart::
+is_title_unknown() const {
+  return _title_unknown;
+}
+
 /**
 /**
  * Returns true if the indicated collector appears anywhere on the chart at
  * Returns true if the indicated collector appears anywhere on the chart at
  * the current time, false otherwise.
  * the current time, false otherwise.

+ 64 - 5
pandatool/src/pstatserver/pStatStripChart.cxx

@@ -284,12 +284,71 @@ get_title_text() {
 }
 }
 
 
 /**
 /**
- * Returns true if get_title_text() has never yet returned an answer, false if
- * it has.
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
  */
  */
-bool PStatStripChart::
-is_title_unknown() const {
-  return _title_unknown;
+std::string PStatStripChart::
+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();
+  }
+
+  std::ostringstream text;
+  text << client_data->get_collector_fullname(collector_index);
+
+  double value;
+  if (collector_index == _collector_index) {
+    value = get_average_net_value();
+  }
+  else {
+    const PStatThreadData *thread_data = _view.get_thread_data();
+    int now_i, then_i;
+    if (!thread_data->get_elapsed_frames(then_i, now_i)) {
+      return text.str();
+    }
+    double now = _time_width + _start_time;
+    double then = now - pstats_average_time;
+
+    double net_value = 0.0f;
+    double net_time = 0.0f;
+
+    // We start with just the portion of frame then_i that actually does fall
+    // within our "then to now" window (usually some portion of it will).
+    const PStatFrameData &frame_data = thread_data->get_frame(then_i);
+    if (frame_data.get_end() > then) {
+      double this_time = (frame_data.get_end() - then);
+      _view.set_to_frame(frame_data);
+
+      const PStatViewLevel *level = _view.get_level(collector_index);
+      if (level != nullptr) {
+        net_value += level->get_net_value() * this_time;
+        net_time += this_time;
+      }
+    }
+    // Then we get all of each of the remaining frames.
+    for (int frame_number = then_i + 1;
+         frame_number <= now_i;
+         frame_number++) {
+      const PStatFrameData &frame_data = thread_data->get_frame(frame_number);
+      double this_time = frame_data.get_net_time();
+      _view.set_to_frame(frame_data);
+
+      const PStatViewLevel *level = _view.get_level(collector_index);
+      if (level != nullptr) {
+        net_value += level->get_net_value() * this_time;
+        net_time += this_time;
+      }
+    }
+
+    if (net_time == 0) {
+      return text.str();
+    }
+    value = net_value / net_time;
+  }
+
+  text << " (" << format_number(value, get_guide_bar_units(), get_guide_bar_unit_name()) << ")";
+  return text.str();
 }
 }
 
 
 /**
 /**

+ 3 - 1
pandatool/src/pstatserver/pStatStripChart.h

@@ -68,8 +68,9 @@ public:
   INLINE int height_to_pixel(double value) const;
   INLINE int height_to_pixel(double value) const;
   INLINE double pixel_to_height(int y) const;
   INLINE double pixel_to_height(int y) const;
 
 
-  std::string get_title_text();
   bool is_title_unknown() const;
   bool is_title_unknown() const;
+  std::string get_title_text();
+  std::string get_label_tooltip(int collector_index) const;
 
 
 protected:
 protected:
   class ColorData {
   class ColorData {
@@ -89,6 +90,7 @@ protected:
   void compute_average_pixel_data(PStatStripChart::FrameData &result,
   void compute_average_pixel_data(PStatStripChart::FrameData &result,
                                   int &then_i, int &now_i, double now);
                                   int &then_i, int &now_i, double now);
   double get_net_value(int frame_number) const;
   double get_net_value(int frame_number) const;
+  double get_net_value(int frame_number, int collector_index) const;
   double get_average_net_value() const;
   double get_average_net_value() const;
 
 
   void changed_size(int xsize, int ysize);
   void changed_size(int xsize, int ysize);

+ 4 - 2
pandatool/src/win-stats/CMakeLists.txt

@@ -4,9 +4,10 @@ endif()
 
 
 set(WINSTATS_HEADERS
 set(WINSTATS_HEADERS
   winStatsChartMenu.h
   winStatsChartMenu.h
+  winStatsFlameGraph.h
   winStatsGraph.h
   winStatsGraph.h
   winStats.h
   winStats.h
-  winStatsLabel.h
+  winStatsLabel.h winStatsLabel.I
   winStatsLabelStack.h
   winStatsLabelStack.h
   winStatsMenuId.h
   winStatsMenuId.h
   winStatsMonitor.h winStatsMonitor.I
   winStatsMonitor.h winStatsMonitor.I
@@ -18,6 +19,7 @@ set(WINSTATS_HEADERS
 set(WINSTATS_SOURCES
 set(WINSTATS_SOURCES
   winStatsChartMenu.cxx
   winStatsChartMenu.cxx
   winStats.cxx
   winStats.cxx
+  winStatsFlameGraph.cxx
   winStatsGraph.cxx
   winStatsGraph.cxx
   winStatsLabel.cxx
   winStatsLabel.cxx
   winStatsLabelStack.cxx
   winStatsLabelStack.cxx
@@ -29,7 +31,7 @@ set(WINSTATS_SOURCES
 
 
 composite_sources(win-stats WINSTATS_SOURCES)
 composite_sources(win-stats WINSTATS_SOURCES)
 add_executable(win-stats ${WINSTATS_HEADERS} ${WINSTATS_SOURCES})
 add_executable(win-stats ${WINSTATS_HEADERS} ${WINSTATS_SOURCES})
-target_link_libraries(win-stats p3progbase p3pstatserver)
+target_link_libraries(win-stats p3progbase p3pstatserver comctl32.lib)
 
 
 # This program is NOT actually called win-stats. It's just pstats.exe
 # This program is NOT actually called win-stats. It's just pstats.exe
 set_target_properties(win-stats PROPERTIES OUTPUT_NAME "pstats")
 set_target_properties(win-stats PROPERTIES OUTPUT_NAME "pstats")

+ 14 - 0
pandatool/src/win-stats/winStats.cxx

@@ -20,6 +20,11 @@
 #define WIN32_LEAN_AND_MEAN 1
 #define WIN32_LEAN_AND_MEAN 1
 #endif
 #endif
 #include <windows.h>
 #include <windows.h>
+#include <commctrl.h>
+#include <shellscalingapi.h>
+
+// Enable common controls version 6, necessary for modern visual styles
+#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
 
 
 static const char *toplevel_class_name = "pstats";
 static const char *toplevel_class_name = "pstats";
 static WinStatsServer *server = nullptr;
 static WinStatsServer *server = nullptr;
@@ -82,6 +87,15 @@ create_toplevel_window(HINSTANCE application) {
 }
 }
 
 
 int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
 int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
+  // Initialize commctl32.dll.
+  INITCOMMONCONTROLSEX icc;
+  icc.dwICC = ICC_WIN95_CLASSES | ICC_STANDARD_CLASSES;
+  icc.dwSize = sizeof(INITCOMMONCONTROLSEX);
+  InitCommonControlsEx(&icc);
+
+  // Signal DPI awareness.
+  SetProcessDPIAware();
+
   HINSTANCE application = GetModuleHandle(nullptr);
   HINSTANCE application = GetModuleHandle(nullptr);
   HWND toplevel_window = create_toplevel_window(application);
   HWND toplevel_window = create_toplevel_window(application);
 
 

+ 21 - 8
pandatool/src/win-stats/winStatsChartMenu.cxx

@@ -125,19 +125,32 @@ do_update() {
     }
     }
   }
   }
 
 
-  // Also a menu item for a piano roll (following a separator).
+  // Also menu items for flame graph and piano roll (following a separator).
   mii.fMask = MIIM_FTYPE;
   mii.fMask = MIIM_FTYPE;
   mii.fType = MFT_SEPARATOR;
   mii.fType = MFT_SEPARATOR;
   InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
   InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
 
 
-  WinStatsMonitor::MenuDef menu_def(_thread_index, -1, false);
-  int menu_id = _monitor->get_menu_id(menu_def);
+  {
+    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 = "Piano Roll";
-  InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
+    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);
+    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 = "Piano Roll";
+    InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
+  }
 }
 }
 
 
 /**
 /**

+ 639 - 0
pandatool/src/win-stats/winStatsFlameGraph.cxx

@@ -0,0 +1,639 @@
+/**
+ * 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 winStatsFlameGraph.cxx
+ * @author rdb
+ * @date 2022-01-28
+ */
+
+#include "winStatsFlameGraph.h"
+#include "winStatsLabel.h"
+#include "winStatsMonitor.h"
+#include "pStatCollectorDef.h"
+
+#include <commctrl.h>
+
+static const int default_flame_graph_width = 800;
+static const int default_flame_graph_height = 150;
+
+bool WinStatsFlameGraph::_window_class_registered = false;
+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),
+                  thread_index, collector_index,
+                  monitor->get_pixel_scale() * default_flame_graph_width / 4,
+                  monitor->get_pixel_scale() * default_flame_graph_height / 4),
+  WinStatsGraph(monitor)
+{
+  _left_margin = _pixel_scale * 2;
+  _right_margin = _pixel_scale * 2;
+  _top_margin = _pixel_scale * 6;
+  _bottom_margin = _pixel_scale * 2;
+
+  // Let's show the units on the guide bar labels.  There's room.
+  set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
+
+  _average_check_box = 0;
+
+  create_window();
+  clear_region();
+}
+
+/**
+ *
+ */
+WinStatsFlameGraph::
+~WinStatsFlameGraph() {
+}
+
+/**
+ * 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 WinStatsFlameGraph::
+new_data(int thread_index, int frame_number) {
+  if (is_title_unknown()) {
+    std::string window_title = get_title_text();
+    if (!is_title_unknown()) {
+      SetWindowText(_window, window_title.c_str());
+    }
+  }
+
+  if (!_pause) {
+    update();
+
+    std::string text = format_number(get_horizontal_scale(), get_guide_bar_units(), get_guide_bar_unit_name());
+    if (_net_value_text != text) {
+      _net_value_text = text;
+      RECT rect;
+      GetClientRect(_window, &rect);
+      rect.bottom = _top_margin;
+      InvalidateRect(_window, &rect, TRUE);
+    }
+  }
+}
+
+/**
+ * Called when it is necessary to redraw the entire graph.
+ */
+void WinStatsFlameGraph::
+force_redraw() {
+  PStatFlameGraph::force_redraw();
+}
+
+/**
+ * Called when the user has resized the window, forcing a resize of the graph.
+ */
+void WinStatsFlameGraph::
+changed_graph_size(int graph_xsize, int graph_ysize) {
+  PStatFlameGraph::changed_size(graph_xsize, graph_ysize);
+}
+
+/**
+ * Called when the user selects a new time units from the monitor pulldown
+ * menu, this should adjust the units for the graph to the indicated mask if
+ * it is a time-based graph.
+ */
+void WinStatsFlameGraph::
+set_time_units(int unit_mask) {
+  int old_unit_mask = get_guide_bar_units();
+  if ((old_unit_mask & (GBU_hz | GBU_ms)) != 0) {
+    unit_mask = unit_mask & (GBU_hz | GBU_ms);
+    unit_mask |= (old_unit_mask & GBU_show_units);
+    set_guide_bar_units(unit_mask);
+
+    RECT rect;
+    GetClientRect(_window, &rect);
+    rect.left = _right_margin;
+    InvalidateRect(_window, &rect, TRUE);
+  }
+}
+
+/**
+ * Called when the user single-clicks on a label.
+ */
+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);
+    }
+  }
+}
+
+/**
+ * Called when the user hovers the mouse over a label.
+ */
+void WinStatsFlameGraph::
+on_enter_label(int collector_index) {
+  if (collector_index != _highlighted_index) {
+    _highlighted_index = collector_index;
+  }
+}
+
+/**
+ * Called when the user's mouse cursor leaves a label.
+ */
+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;
+    }
+    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));
+}
+
+/**
+ * Calls update_guide_bars with parameters suitable to this kind of graph.
+ */
+void WinStatsFlameGraph::
+normal_guide_bars() {
+  // We want vaguely 100 pixels between guide bars.
+  int num_bars = get_xsize() / (_pixel_scale * 25);
+
+  _guide_bars.clear();
+
+  double dist = get_horizontal_scale() / num_bars;
+
+  for (int i = 1; i < num_bars; ++i) {
+    _guide_bars.push_back(make_guide_bar(i * dist));
+  }
+
+  _guide_bars_changed = true;
+}
+
+/**
+ * Erases the chart area.
+ */
+void WinStatsFlameGraph::
+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 WinStatsFlameGraph::
+begin_draw() {
+  clear_region();
+
+  // Draw in the guide bars.
+  int num_guide_bars = get_num_guide_bars();
+  for (int i = 0; i < num_guide_bars; i++) {
+    draw_guide_bar(_bitmap_dc, get_guide_bar(i));
+  }
+}
+
+/**
+ * Called after all the bars have been drawn, this triggers a refresh event to
+ * draw it to the window.
+ */
+void WinStatsFlameGraph::
+end_draw() {
+  InvalidateRect(_graph_window, nullptr, FALSE);
+}
+
+/**
+ * Called at the end of the draw cycle.
+ */
+void WinStatsFlameGraph::
+idle() {
+}
+
+/**
+ *
+ */
+LONG WinStatsFlameGraph::
+window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
+  switch (msg) {
+  case WM_LBUTTONDOWN:
+    if (_potential_drag_mode == DM_new_guide_bar) {
+      set_drag_mode(DM_new_guide_bar);
+      SetCapture(_graph_window);
+      return 0;
+    }
+    break;
+
+  case WM_COMMAND:
+    switch (LOWORD(wparam)) {
+    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);
+        return 0;
+      }
+      break;
+    }
+    break;
+
+  default:
+    break;
+  }
+
+  return WinStatsGraph::window_proc(hwnd, msg, wparam, lparam);
+}
+
+/**
+ *
+ */
+LONG WinStatsFlameGraph::
+graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
+  switch (msg) {
+  case WM_LBUTTONDOWN:
+    if (_potential_drag_mode == DM_guide_bar && _drag_guide_bar >= 0) {
+      set_drag_mode(DM_guide_bar);
+      int16_t x = LOWORD(lparam);
+      _drag_start_x = x;
+      SetCapture(_graph_window);
+      return 0;
+    }
+    break;
+
+  case WM_MOUSEMOVE:
+    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);
+      if (x >= 0 && x < get_xsize()) {
+        set_drag_mode(DM_guide_bar);
+        _drag_guide_bar = add_user_guide_bar(pixel_to_height(x));
+        return 0;
+      }
+    }
+    else if (_drag_mode == DM_guide_bar) {
+      int16_t x = LOWORD(lparam);
+      move_user_guide_bar(_drag_guide_bar, pixel_to_height(x));
+      return 0;
+    }
+    break;
+
+  case WM_LBUTTONUP:
+    if (_drag_mode == DM_guide_bar) {
+      int16_t x = LOWORD(lparam);
+      if (x < 0 || x >= get_xsize()) {
+        remove_user_guide_bar(_drag_guide_bar);
+      } else {
+        move_user_guide_bar(_drag_guide_bar, pixel_to_height(x));
+      }
+      set_drag_mode(DM_none);
+      ReleaseCapture();
+      return 0;
+    }
+    break;
+
+  case WM_LBUTTONDBLCLK:
+    {
+      // Clicking on whitespace in the graph goes to the parent.
+      on_click_label(get_collector_index());
+      return 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 WinStatsFlameGraph::
+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 - _pixel_scale / 2;
+
+  int i;
+  int num_guide_bars = get_num_guide_bars();
+  for (i = 0; i < num_guide_bars; i++) {
+    draw_guide_label(hdc, y, get_guide_bar(i));
+  }
+
+  int num_user_guide_bars = get_num_user_guide_bars();
+  for (i = 0; i < num_user_guide_bars; i++) {
+    draw_guide_label(hdc, y, get_user_guide_bar(i));
+  }
+
+  RECT rect;
+  GetClientRect(_window, &rect);
+
+  // Now draw the "net value" label at the top.
+  SetTextAlign(hdc, TA_RIGHT | TA_BOTTOM);
+  SetTextColor(hdc, RGB(0, 0, 0));
+  TextOut(hdc, rect.right - _right_margin, y,
+          _net_value_text.data(), _net_value_text.length());
+}
+
+/**
+ * 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 WinStatsFlameGraph::
+additional_graph_window_paint(HDC hdc) {
+  int num_user_guide_bars = get_num_user_guide_bars();
+  for (int i = 0; i < num_user_guide_bars; i++) {
+    draw_guide_bar(hdc, get_user_guide_bar(i));
+  }
+}
+
+/**
+ * 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 WinStatsFlameGraph::
+consider_drag_start(int mouse_x, int mouse_y, int width, int height) {
+  if (mouse_y >= _graph_top && mouse_y < _graph_top + get_ysize()) {
+    if (mouse_x >= _graph_left && mouse_x < _graph_left + get_xsize()) {
+      // See if the mouse is over a user-defined guide bar.
+      int x = mouse_x - _graph_left;
+      double from_height = pixel_to_height(x - 2);
+      double to_height = pixel_to_height(x + 2);
+      _drag_guide_bar = find_user_guide_bar(from_height, to_height);
+      if (_drag_guide_bar >= 0) {
+        return DM_guide_bar;
+      }
+
+    } else if (mouse_x < _left_margin - 2 ||
+               mouse_x > width - _right_margin + 2) {
+      // The mouse is left or right of the graph; maybe create a new guide
+      // bar.
+      return DM_new_guide_bar;
+    }
+  }
+
+  // Don't upcall; there's no point resizing the margins.
+  return DM_none;
+}
+
+/**
+ * Repositions the graph child window within the parent window according to
+ * the _margin variables.
+ */
+void WinStatsFlameGraph::
+move_graph_window(int graph_left, int graph_top, int graph_xsize, int graph_ysize) {
+  WinStatsGraph::move_graph_window(graph_left, graph_top, graph_xsize, graph_ysize);
+  if (_average_check_box != 0) {
+    SIZE size;
+    SendMessage(_average_check_box, BCM_GETIDEALSIZE, 0, (LPARAM)&size);
+
+    SetWindowPos(_average_check_box, 0,
+                 _left_margin, _top_margin - size.cy - _pixel_scale / 2,
+                 size.cx, size.cy,
+                 SWP_NOZORDER | SWP_SHOWWINDOW);
+    InvalidateRect(_average_check_box, nullptr, TRUE);
+  }
+}
+
+/**
+ * Draws the line for the indicated guide bar on the graph.
+ */
+void WinStatsFlameGraph::
+draw_guide_bar(HDC hdc, const PStatGraph::GuideBar &bar) {
+  int x = height_to_pixel(bar._height);
+
+  if (x > 0 && x < get_xsize() - 1) {
+    // Only draw it if it's not too close to either edge.
+    switch (bar._style) {
+    case GBS_target:
+      SelectObject(hdc, _light_pen);
+      break;
+
+    case GBS_user:
+      SelectObject(hdc, _user_guide_bar_pen);
+      break;
+
+    case GBS_normal:
+      SelectObject(hdc, _dark_pen);
+      break;
+    }
+    MoveToEx(hdc, x, 0, nullptr);
+    LineTo(hdc, x, get_ysize());
+  }
+}
+
+/**
+ * Draws the text for the indicated guide bar label at the top of the graph.
+ */
+void WinStatsFlameGraph::
+draw_guide_label(HDC hdc, int y, const PStatGraph::GuideBar &bar) {
+  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, _dark_color);
+    break;
+  }
+
+  int x = height_to_pixel(bar._height);
+  const std::string &label = bar._label;
+  SIZE size;
+  GetTextExtentPoint32(hdc, label.data(), label.length(), &size);
+
+  if (bar._style != GBS_user) {
+    double from_height = pixel_to_height(x - size.cx);
+    double to_height = pixel_to_height(x + size.cx);
+    if (find_user_guide_bar(from_height, to_height) >= 0) {
+      // Omit the label: there's a user-defined guide bar in the same space.
+      return;
+    }
+  }
+
+  int this_x = _graph_left + x - size.cx / 2;
+  if (x >= 0 && x < get_xsize()) {
+    TextOut(hdc, this_x, y,
+            label.data(), label.length());
+  }
+}
+
+/**
+ * Creates the window for this strip chart.
+ */
+void WinStatsFlameGraph::
+create_window() {
+  if (_window) {
+    return;
+  }
+
+  HINSTANCE application = GetModuleHandle(nullptr);
+  register_window_class(application);
+
+  std::string window_title = get_title_text();
+
+  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 =
+    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);
+  if (!_window) {
+    nout << "Could not create FlameGraph window!\n";
+    exit(1);
+  }
+
+  SetWindowLongPtr(_window, 0, (LONG_PTR)this);
+
+  _average_check_box =
+    CreateWindow(WC_BUTTON, "Average", WS_CHILD | BS_AUTOCHECKBOX,
+                 0, 0, 0, 0,
+                 _window, nullptr, application, 0);
+  SendMessage(_average_check_box, WM_SETFONT,
+              (WPARAM)WinStatsGraph::_monitor->get_font(), TRUE);
+
+  if (get_average_mode()) {
+    SendMessage(_average_check_box, BM_SETCHECK, BST_CHECKED, 0);
+  }
+
+  // Ensure that the window is on top of the stack.
+  SetWindowPos(_window, HWND_TOP, 0, 0, 0, 0,
+               SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
+}
+
+/**
+ * Registers the window class for the FlameGraph window, if it has not already
+ * been registered.
+ */
+void WinStatsFlameGraph::
+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(WinStatsFlameGraph *);
+
+  if (!RegisterClass(&wc)) {
+    nout << "Could not register FlameGraph window class!\n";
+    exit(1);
+  }
+
+  _window_class_registered = true;
+}
+
+/**
+ *
+ */
+LONG WINAPI WinStatsFlameGraph::
+static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
+  WinStatsFlameGraph *self = (WinStatsFlameGraph *)GetWindowLongPtr(hwnd, 0);
+  if (self != nullptr && self->_window == hwnd) {
+    return self->window_proc(hwnd, msg, wparam, lparam);
+  } else {
+    return DefWindowProc(hwnd, msg, wparam, lparam);
+  }
+}

+ 80 - 0
pandatool/src/win-stats/winStatsFlameGraph.h

@@ -0,0 +1,80 @@
+/**
+ * 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 winStatsFlameGraph.h
+ * @author rdb
+ * @date 2022-01-28
+ */
+
+#ifndef WINSTATSFLAMEGRAPH_H
+#define WINSTATSFLAMEGRAPH_H
+
+#include "pandatoolbase.h"
+
+#include "winStatsGraph.h"
+#include "pStatFlameGraph.h"
+
+class WinStatsLabel;
+
+/**
+ * A window that draws a flame chart, which shows the collectors explicitly
+ * stopping and starting, one frame at a time.
+ */
+class WinStatsFlameGraph : public PStatFlameGraph, public WinStatsGraph {
+public:
+  WinStatsFlameGraph(WinStatsMonitor *monitor, int thread_index,
+                     int collector_index=0);
+  virtual ~WinStatsFlameGraph();
+
+  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);
+
+  virtual void set_time_units(int unit_mask);
+  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 end_draw();
+  virtual void idle();
+
+  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 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:
+  void draw_guide_bar(HDC hdc, const GuideBar &bar);
+  void draw_guide_label(HDC hdc, int y, const PStatGraph::GuideBar &bar);
+  void create_window();
+  static void register_window_class(HINSTANCE application);
+
+  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;
+
+  static bool _window_class_registered;
+  static const char * const _window_class_name;
+};
+
+#endif

+ 107 - 84
pandatool/src/win-stats/winStatsGraph.cxx

@@ -14,9 +14,11 @@
 #include "winStatsGraph.h"
 #include "winStatsGraph.h"
 #include "winStatsMonitor.h"
 #include "winStatsMonitor.h"
 #include "winStatsLabelStack.h"
 #include "winStatsLabelStack.h"
+#include "convert_srgb.h"
 
 
-bool WinStatsGraph::_graph_window_class_registered = false;
-const char * const WinStatsGraph::_graph_window_class_name = "graph";
+#include <commctrl.h>
+
+#define IDC_GRAPH 100
 
 
 DWORD WinStatsGraph::graph_window_style =
 DWORD WinStatsGraph::graph_window_style =
 WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_OVERLAPPEDWINDOW | WS_VISIBLE;
 WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_OVERLAPPEDWINDOW | WS_VISIBLE;
@@ -40,6 +42,8 @@ WinStatsGraph(WinStatsMonitor *monitor) :
   _bitmap_xsize = 0;
   _bitmap_xsize = 0;
   _bitmap_ysize = 0;
   _bitmap_ysize = 0;
 
 
+  _pixel_scale = monitor->get_pixel_scale();
+
   _dark_color = RGB(51, 51, 51);
   _dark_color = RGB(51, 51, 51);
   _light_color = RGB(154, 154, 154);
   _light_color = RGB(154, 154, 154);
   _user_guide_bar_color = RGB(130, 150, 255);
   _user_guide_bar_color = RGB(130, 150, 255);
@@ -66,11 +70,11 @@ WinStatsGraph::
   DeleteObject(_light_pen);
   DeleteObject(_light_pen);
   DeleteObject(_user_guide_bar_pen);
   DeleteObject(_user_guide_bar_pen);
 
 
-  Brushes::iterator bi;
-  for (bi = _brushes.begin(); bi != _brushes.end(); ++bi) {
-    HBRUSH brush = (*bi).second;
-    DeleteObject(brush);
+  for (auto &item : _brushes) {
+    DeleteObject(item.second.first);
+    DeleteObject(item.second.second);
   }
   }
+  _brushes.clear();
 
 
   if (_graph_window) {
   if (_graph_window) {
     DestroyWindow(_graph_window);
     DestroyWindow(_graph_window);
@@ -97,13 +101,6 @@ void WinStatsGraph::
 new_data(int thread_index, int frame_number) {
 new_data(int thread_index, int frame_number) {
 }
 }
 
 
-/**
- * Called when it is necessary to redraw the entire graph.
- */
-void WinStatsGraph::
-force_redraw() {
-}
-
 /**
 /**
  * Called when the user has resized the window, forcing a resize of the graph.
  * Called when the user has resized the window, forcing a resize of the graph.
  */
  */
@@ -150,7 +147,46 @@ user_guide_bars_changed() {
  * Called when the user single-clicks on a label.
  * Called when the user single-clicks on a label.
  */
  */
 void WinStatsGraph::
 void WinStatsGraph::
-clicked_label(int collector_index) {
+on_click_label(int collector_index) {
+}
+
+/**
+ * Called when the user hovers the mouse over a label.
+ */
+void WinStatsGraph::
+on_enter_label(int collector_index) {
+  if (collector_index != _highlighted_index) {
+    _highlighted_index = collector_index;
+    force_redraw();
+  }
+}
+
+/**
+ * Called when the user's mouse cursor leaves a label.
+ */
+void WinStatsGraph::
+on_leave_label(int collector_index) {
+  if (collector_index == _highlighted_index && collector_index != -1) {
+    _highlighted_index = -1;
+    force_redraw();
+  }
+}
+
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string WinStatsGraph::
+get_label_tooltip(int collector_index) const {
+  return std::string();
+}
+
+/**
+ * Returns the window handle of the surrounding window.
+ */
+HWND WinStatsGraph::
+get_window() {
+  return _window;
 }
 }
 
 
 /**
 /**
@@ -184,8 +220,8 @@ move_label_stack() {
     RECT rect;
     RECT rect;
     GetClientRect(_window, &rect);
     GetClientRect(_window, &rect);
 
 
-    rect.left += 8;
-    rect.right = _left_margin - 8;
+    rect.left += _pixel_scale * 2;
+    rect.right = _left_margin - _pixel_scale * 2;
     rect.bottom -= _bottom_margin;
     rect.bottom -= _bottom_margin;
 
 
     _label_stack.set_pos(rect.left, rect.top,
     _label_stack.set_pos(rect.left, rect.top,
@@ -197,22 +233,27 @@ move_label_stack() {
  * Returns a brush suitable for drawing in the indicated collector's color.
  * Returns a brush suitable for drawing in the indicated collector's color.
  */
  */
 HBRUSH WinStatsGraph::
 HBRUSH WinStatsGraph::
-get_collector_brush(int collector_index) {
+get_collector_brush(int collector_index, bool highlight) {
   Brushes::iterator bi;
   Brushes::iterator bi;
   bi = _brushes.find(collector_index);
   bi = _brushes.find(collector_index);
   if (bi != _brushes.end()) {
   if (bi != _brushes.end()) {
-    return (*bi).second;
+    return highlight ? (*bi).second.second : (*bi).second.first;
   }
   }
 
 
   // Ask the monitor what color this guy should be.
   // Ask the monitor what color this guy should be.
   LRGBColor rgb = _monitor->get_collector_color(collector_index);
   LRGBColor rgb = _monitor->get_collector_color(collector_index);
-  int r = (int)(rgb[0] * 255.0f);
-  int g = (int)(rgb[1] * 255.0f);
-  int b = (int)(rgb[2] * 255.0f);
+  int r = (int)encode_sRGB_uchar(rgb[0]);
+  int g = (int)encode_sRGB_uchar(rgb[1]);
+  int b = (int)encode_sRGB_uchar(rgb[2]);
   HBRUSH brush = CreateSolidBrush(RGB(r, g, b));
   HBRUSH brush = CreateSolidBrush(RGB(r, g, b));
 
 
-  _brushes[collector_index] = brush;
-  return brush;
+  int hr = (int)encode_sRGB_uchar(rgb[0] * 0.75f);
+  int hg = (int)encode_sRGB_uchar(rgb[1] * 0.75f);
+  int hb = (int)encode_sRGB_uchar(rgb[2] * 0.75f);
+  HBRUSH hbrush = CreateSolidBrush(RGB(hr, hg, hb));
+
+  _brushes[collector_index] = std::make_pair(brush, hbrush);
+  return highlight ? hbrush : brush;
 }
 }
 
 
 /**
 /**
@@ -226,6 +267,20 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     close();
     close();
     break;
     break;
 
 
+  case WM_GETMINMAXINFO:
+    {
+      WINDOWINFO winfo;
+      GetWindowInfo(hwnd, &winfo);
+      MINMAXINFO &minmax = *(MINMAXINFO *)lparam;
+      minmax.ptMinTrackSize.x = (winfo.rcClient.left - winfo.rcWindow.left)
+                              + (winfo.rcWindow.right - winfo.rcClient.right)
+                              + (_right_margin + _left_margin);
+      minmax.ptMinTrackSize.y = (winfo.rcClient.top - winfo.rcWindow.top)
+                              + (winfo.rcWindow.bottom - winfo.rcClient.bottom)
+                              + (_bottom_margin + _top_margin);
+      return 0;
+    }
+
   case WM_SIZE:
   case WM_SIZE:
     move_label_stack();
     move_label_stack();
     InvalidateRect(hwnd, nullptr, TRUE);
     InvalidateRect(hwnd, nullptr, TRUE);
@@ -319,8 +374,6 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
       rect.bottom -= _bottom_margin;
       rect.bottom -= _bottom_margin;
 
 
       if (rect.right > rect.left && rect.bottom > rect.top) {
       if (rect.right > rect.left && rect.bottom > rect.top) {
-        DrawEdge(hdc, &rect, EDGE_SUNKEN, BF_RECT | BF_ADJUST);
-
         int graph_xsize = rect.right - rect.left;
         int graph_xsize = rect.right - rect.left;
         int graph_ysize = rect.bottom - rect.top;
         int graph_ysize = rect.bottom - rect.top;
         if (_bitmap_dc == 0 ||
         if (_bitmap_dc == 0 ||
@@ -339,6 +392,21 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
       return 0;
       return 0;
     }
     }
 
 
+  case WM_DRAWITEM:
+    if (wparam == IDC_GRAPH) {
+      const DRAWITEMSTRUCT &dis = *(DRAWITEMSTRUCT *)lparam;
+
+      // Repaint the graph by copying the backing pixmap in.
+      BitBlt(dis.hDC, 0, 0,
+             _bitmap_xsize, _bitmap_ysize,
+             _bitmap_dc, 0, 0,
+             SRCCOPY);
+
+      additional_graph_window_paint(dis.hDC);
+      return 0;
+    }
+    break;
+
   default:
   default:
     break;
     break;
   }
   }
@@ -357,6 +425,10 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     force_redraw();
     force_redraw();
     break;
     break;
 
 
+  case WM_NCHITTEST:
+    // Necessary for mouse events to work; default returns HTTRANSPARENT
+    return HTCLIENT;
+
   case WM_LBUTTONDOWN:
   case WM_LBUTTONDOWN:
     // Vector any uncaught WM_LBUTTONDOWN into the main window, so we can drag
     // Vector any uncaught WM_LBUTTONDOWN into the main window, so we can drag
     // margins, etc.
     // margins, etc.
@@ -372,28 +444,11 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     ReleaseCapture();
     ReleaseCapture();
     break;
     break;
 
 
-  case WM_PAINT:
-    {
-      // Repaint the graph by copying the backing pixmap in.
-      PAINTSTRUCT ps;
-      HDC hdc = BeginPaint(hwnd, &ps);
-
-      BitBlt(hdc, 0, 0,
-             _bitmap_xsize, _bitmap_ysize,
-             _bitmap_dc, 0, 0,
-             SRCCOPY);
-
-      additional_graph_window_paint(hdc);
-
-      EndPaint(hwnd, &ps);
-      return 0;
-    }
-
   default:
   default:
     break;
     break;
   }
   }
 
 
-  return DefWindowProc(hwnd, msg, wparam, lparam);
+  return DefSubclassProc(hwnd, msg, wparam, lparam);
 }
 }
 
 
 /**
 /**
@@ -506,61 +561,29 @@ create_graph_window() {
   }
   }
 
 
   HINSTANCE application = GetModuleHandle(nullptr);
   HINSTANCE application = GetModuleHandle(nullptr);
-  register_graph_window_class(application);
 
 
-  std::string window_title = "graph";
-  DWORD window_style = WS_CHILD | WS_CLIPSIBLINGS;
+  DWORD window_style = WS_CHILD | WS_CLIPSIBLINGS |
+                       SS_SUNKEN | SS_OWNERDRAW;
 
 
   _graph_window =
   _graph_window =
-    CreateWindow(_graph_window_class_name, window_title.c_str(), window_style,
-                 0, 0, 0, 0,
-                 _window, nullptr, application, 0);
+    CreateWindow(WC_STATIC, "", window_style, 0, 0, 0, 0,
+                 _window, (HMENU)IDC_GRAPH, application, 0);
   if (!_graph_window) {
   if (!_graph_window) {
     nout << "Could not create graph window!\n";
     nout << "Could not create graph window!\n";
     exit(1);
     exit(1);
   }
   }
 
 
-  SetWindowLongPtr(_graph_window, 0, (LONG_PTR)this);
-}
-
-/**
- * Registers the window class for the stripChart window, if it has not already
- * been registered.
- */
-void WinStatsGraph::
-register_graph_window_class(HINSTANCE application) {
-  if (_graph_window_class_registered) {
-    return;
-  }
-
-  WNDCLASS wc;
-
-  ZeroMemory(&wc, sizeof(WNDCLASS));
-  wc.style = CS_DBLCLKS;
-  wc.lpfnWndProc = (WNDPROC)static_graph_window_proc;
-  wc.hInstance = application;
-  wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
-  wc.hbrBackground = nullptr;
-  wc.lpszMenuName = nullptr;
-  wc.lpszClassName = _graph_window_class_name;
-
-  // Reserve space to associate the this pointer with the window.
-  wc.cbWndExtra = sizeof(WinStatsGraph *);
-
-  if (!RegisterClass(&wc)) {
-    nout << "Could not register graph window class!\n";
-    exit(1);
-  }
+  EnableWindow(_graph_window, TRUE);
 
 
-  _graph_window_class_registered = true;
+  SetWindowSubclass(_graph_window, &static_graph_subclass_proc, 1234, (DWORD_PTR)this);
 }
 }
 
 
 /**
 /**
  *
  *
  */
  */
-LONG WINAPI WinStatsGraph::
-static_graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
-  WinStatsGraph *self = (WinStatsGraph *)GetWindowLongPtr(hwnd, 0);
+LRESULT WINAPI WinStatsGraph::
+static_graph_subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam, UINT_PTR subclass, DWORD_PTR ref_data) {
+  WinStatsGraph *self = (WinStatsGraph *)ref_data;
   if (self != nullptr && self->_graph_window == hwnd) {
   if (self != nullptr && self->_graph_window == hwnd) {
     return self->graph_window_proc(hwnd, msg, wparam, lparam);
     return self->graph_window_proc(hwnd, msg, wparam, lparam);
   } else {
   } else {

+ 13 - 9
pandatool/src/win-stats/winStatsGraph.h

@@ -48,7 +48,7 @@ public:
 
 
   virtual void new_collector(int collector_index);
   virtual void new_collector(int collector_index);
   virtual void new_data(int thread_index, int frame_number);
   virtual void new_data(int thread_index, int frame_number);
-  virtual void force_redraw();
+  virtual void force_redraw()=0;
   virtual void changed_graph_size(int graph_xsize, int graph_ysize);
   virtual void changed_graph_size(int graph_xsize, int graph_ysize);
 
 
   virtual void set_time_units(int unit_mask);
   virtual void set_time_units(int unit_mask);
@@ -56,7 +56,12 @@ public:
   void set_pause(bool pause);
   void set_pause(bool pause);
 
 
   void user_guide_bars_changed();
   void user_guide_bars_changed();
-  virtual void clicked_label(int collector_index);
+  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;
+
+  HWND get_window();
 
 
 protected:
 protected:
   void close();
   void close();
@@ -64,7 +69,7 @@ protected:
   void setup_label_stack();
   void setup_label_stack();
   void move_label_stack();
   void move_label_stack();
 
 
-  HBRUSH get_collector_brush(int collector_index);
+  HBRUSH get_collector_brush(int collector_index, bool highlight = false);
 
 
   LONG window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
   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 LONG graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
@@ -80,7 +85,7 @@ protected:
 
 
 protected:
 protected:
   // Table of brushes for our various collectors.
   // Table of brushes for our various collectors.
-  typedef pmap<int, HBRUSH> Brushes;
+  typedef pmap<int, std::pair<HBRUSH, HBRUSH> > Brushes;
   Brushes _brushes;
   Brushes _brushes;
 
 
   WinStatsMonitor *_monitor;
   WinStatsMonitor *_monitor;
@@ -98,6 +103,7 @@ protected:
   int _bitmap_xsize, _bitmap_ysize;
   int _bitmap_xsize, _bitmap_ysize;
   int _left_margin, _right_margin;
   int _left_margin, _right_margin;
   int _top_margin, _bottom_margin;
   int _top_margin, _bottom_margin;
+  int _pixel_scale;
 
 
   COLORREF _dark_color;
   COLORREF _dark_color;
   COLORREF _light_color;
   COLORREF _light_color;
@@ -112,18 +118,16 @@ protected:
   double _drag_scale_start;
   double _drag_scale_start;
   int _drag_guide_bar;
   int _drag_guide_bar;
 
 
+  int _highlighted_index = -1;
+
   bool _pause;
   bool _pause;
 
 
 private:
 private:
   void setup_bitmap(int xsize, int ysize);
   void setup_bitmap(int xsize, int ysize);
   void release_bitmap();
   void release_bitmap();
   void create_graph_window();
   void create_graph_window();
-  static void register_graph_window_class(HINSTANCE application);
-
-  static LONG WINAPI static_graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
 
 
-  static bool _graph_window_class_registered;
-  static const char * const _graph_window_class_name;
+  static LRESULT WINAPI static_graph_subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam, UINT_PTR subclass, DWORD_PTR ref_data);
 
 
 protected:
 protected:
   static DWORD graph_window_style;
   static DWORD graph_window_style;

+ 68 - 0
pandatool/src/win-stats/winStatsLabel.I

@@ -0,0 +1,68 @@
+/**
+ * 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 winStatsLabel.I
+ * @author rdb
+ * @date 2022-01-29
+ */
+
+/**
+ * Returns the x position of the label on its parent.
+ */
+INLINE int WinStatsLabel::
+get_x() const {
+  return _x;
+}
+
+/**
+ * Returns the y position of the label on its parent.
+ */
+INLINE int WinStatsLabel::
+get_y() const {
+  return _y;
+}
+
+/**
+ * Returns the width of the label as we requested it.
+ */
+INLINE int WinStatsLabel::
+get_width() const {
+  return _width;
+}
+
+/**
+ * Returns the height of the label as we requested it.
+ */
+INLINE int WinStatsLabel::
+get_height() const {
+  return _height;
+}
+
+/**
+ * Returns the width the label would really prefer to be.
+ */
+INLINE int WinStatsLabel::
+get_ideal_width() const {
+  return _ideal_width;
+}
+
+/**
+ * Returns the collector this label represents.
+ */
+INLINE int WinStatsLabel::
+get_collector_index() const {
+  return _collector_index;
+}
+
+/**
+ * Returns true if the visual highlight for this label is enabled.
+ */
+INLINE bool WinStatsLabel::
+get_highlight() const {
+  return _highlight;
+}

+ 108 - 90
pandatool/src/win-stats/winStatsLabel.cxx

@@ -14,6 +14,9 @@
 #include "winStatsLabel.h"
 #include "winStatsLabel.h"
 #include "winStatsMonitor.h"
 #include "winStatsMonitor.h"
 #include "winStatsGraph.h"
 #include "winStatsGraph.h"
+#include "convert_srgb.h"
+
+#include <commctrl.h>
 
 
 int WinStatsLabel::_left_margin = 2;
 int WinStatsLabel::_left_margin = 2;
 int WinStatsLabel::_right_margin = 2;
 int WinStatsLabel::_right_margin = 2;
@@ -28,38 +31,45 @@ const char * const WinStatsLabel::_window_class_name = "label";
  */
  */
 WinStatsLabel::
 WinStatsLabel::
 WinStatsLabel(WinStatsMonitor *monitor, WinStatsGraph *graph,
 WinStatsLabel(WinStatsMonitor *monitor, WinStatsGraph *graph,
-              int thread_index, int collector_index, bool use_fullname) :
+              int thread_index, int collector_index, bool use_fullname,
+              bool align_right) :
   _monitor(monitor),
   _monitor(monitor),
   _graph(graph),
   _graph(graph),
   _thread_index(thread_index),
   _thread_index(thread_index),
-  _collector_index(collector_index)
+  _collector_index(collector_index),
+  _align_right(align_right),
+  _window(0),
+  _tooltip_window(0)
 {
 {
-  _window = 0;
-  if (use_fullname) {
-    _text = _monitor->get_client_data()->get_collector_fullname(_collector_index);
-  } else {
-    _text = _monitor->get_client_data()->get_collector_name(_collector_index);
-  }
+  update_text(use_fullname);
 
 
   LRGBColor rgb = _monitor->get_collector_color(_collector_index);
   LRGBColor rgb = _monitor->get_collector_color(_collector_index);
-  int r = (int)(rgb[0] * 255.0f);
-  int g = (int)(rgb[1] * 255.0f);
-  int b = (int)(rgb[2] * 255.0f);
-  _bg_color = RGB(r, g, b);
+  int r = (int)encode_sRGB_uchar(rgb[0]);
+  int g = (int)encode_sRGB_uchar(rgb[1]);
+  int b = (int)encode_sRGB_uchar(rgb[2]);
   _bg_brush = CreateSolidBrush(RGB(r, g, b));
   _bg_brush = CreateSolidBrush(RGB(r, g, b));
 
 
+  // Calculate the color when it is highlighted.
+  int hr = (int)encode_sRGB_uchar(rgb[0] * 0.75f);
+  int hg = (int)encode_sRGB_uchar(rgb[1] * 0.75f);
+  int hb = (int)encode_sRGB_uchar(rgb[2] * 0.75f);
+  _highlight_bg_brush = CreateSolidBrush(RGB(hr, hg, hb));
+
   // Should our foreground be black or white?
   // Should our foreground be black or white?
   double bright =
   double bright =
-    rgb[0] * 0.299 +
-    rgb[1] * 0.587 +
-    rgb[2] * 0.114;
+    rgb[0] * 0.2126 +
+    rgb[1] * 0.7152 +
+    rgb[2] * 0.0722;
 
 
   if (bright >= 0.5) {
   if (bright >= 0.5) {
     _fg_color = RGB(0, 0, 0);
     _fg_color = RGB(0, 0, 0);
-    _highlight_brush = (HBRUSH)GetStockObject(BLACK_BRUSH);
   } else {
   } else {
     _fg_color = RGB(255, 255, 255);
     _fg_color = RGB(255, 255, 255);
-    _highlight_brush = (HBRUSH)GetStockObject(WHITE_BRUSH);
+  }
+  if (bright >= 0.5 * 0.75) {
+    _highlight_fg_color = RGB(0, 0, 0);
+  } else {
+    _highlight_fg_color = RGB(255, 255, 255);
   }
   }
 
 
   _x = 0;
   _x = 0;
@@ -77,6 +87,10 @@ WinStatsLabel(WinStatsMonitor *monitor, WinStatsGraph *graph,
 WinStatsLabel::
 WinStatsLabel::
 ~WinStatsLabel() {
 ~WinStatsLabel() {
   if (_window) {
   if (_window) {
+    if (_tooltip_window) {
+      DestroyWindow(_tooltip_window);
+      _tooltip_window = 0;
+    }
     DestroyWindow(_window);
     DestroyWindow(_window);
     _window = 0;
     _window = 0;
   }
   }
@@ -96,7 +110,7 @@ setup(HWND parent_window) {
   create_window(parent_window);
   create_window(parent_window);
 
 
   HDC hdc = GetDC(_window);
   HDC hdc = GetDC(_window);
-  HFONT hfnt = (HFONT)GetStockObject(ANSI_VAR_FONT);
+  HFONT hfnt = _monitor->get_font();
   SelectObject(hdc, hfnt);
   SelectObject(hdc, hfnt);
 
 
   SIZE size;
   SIZE size;
@@ -113,59 +127,13 @@ setup(HWND parent_window) {
  */
  */
 void WinStatsLabel::
 void WinStatsLabel::
 set_pos(int x, int y, int width) {
 set_pos(int x, int y, int width) {
-  _x = x;
-  _y = y;
-  _width = width;
-  SetWindowPos(_window, 0, x, y - _height, _width, _height,
-               SWP_NOZORDER | SWP_SHOWWINDOW);
-}
-
-/**
- * Returns the x position of the label on its parent.
- */
-int WinStatsLabel::
-get_x() const {
-  return _x;
-}
-
-/**
- * Returns the y position of the label on its parent.
- */
-int WinStatsLabel::
-get_y() const {
-  return _y;
-}
-
-/**
- * Returns the width of the label as we requested it.
- */
-int WinStatsLabel::
-get_width() const {
-  return _width;
-}
-
-/**
- * Returns the height of the label as we requested it.
- */
-int WinStatsLabel::
-get_height() const {
-  return _height;
-}
-
-/**
- * Returns the width the label would really prefer to be.
- */
-int WinStatsLabel::
-get_ideal_width() const {
-  return _ideal_width;
-}
-
-/**
- * Returns the collector this label represents.
- */
-int WinStatsLabel::
-get_collector_index() const {
-  return _collector_index;
+  if (x != _x || y != _y || width != _width) {
+    _x = x;
+    _y = y;
+    _width = width;
+    SetWindowPos(_window, 0, x, y - _height, _width, _height,
+                 SWP_NOZORDER | SWP_SHOWWINDOW);
+  }
 }
 }
 
 
 /**
 /**
@@ -180,11 +148,29 @@ set_highlight(bool highlight) {
 }
 }
 
 
 /**
 /**
- * Returns true if the visual highlight for this label is enabled.
+ * Set to true if the full name of the collector should be shown.
  */
  */
-bool WinStatsLabel::
-get_highlight() const {
-  return _highlight;
+void WinStatsLabel::
+update_text(bool use_fullname) {
+  const PStatClientData *client_data = _monitor->get_client_data();
+  _tooltip_text = client_data->get_collector_fullname(_collector_index);
+  if (use_fullname) {
+    _text = _tooltip_text;
+  } else {
+    _text = client_data->get_collector_name(_collector_index);
+  }
+
+  // Recalculate the dimensions.
+  if (_window) {
+    HDC hdc = GetDC(_window);
+    HFONT hfnt = _monitor->get_font();
+    SelectObject(hdc, hfnt);
+
+    SIZE size;
+    GetTextExtentPoint32(hdc, _text.data(), _text.length(), &size);
+    _height = size.cy + _top_margin + _bottom_margin;
+    _ideal_width = size.cx + _left_margin + _right_margin;
+  }
 }
 }
 
 
 /**
 /**
@@ -220,6 +206,25 @@ create_window(HWND parent_window) {
   }
   }
 
 
   SetWindowLongPtr(_window, 0, (LONG_PTR)this);
   SetWindowLongPtr(_window, 0, (LONG_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)_window;
+    info.lpszText = LPSTR_TEXTCALLBACK;
+    SendMessage(_tooltip_window, TTM_ADDTOOL, 0, (LPARAM)&info);
+  }
 }
 }
 
 
 /**
 /**
@@ -274,13 +279,16 @@ LONG WinStatsLabel::
 window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
 window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
   switch (msg) {
   switch (msg) {
   case WM_LBUTTONDBLCLK:
   case WM_LBUTTONDBLCLK:
-    _graph->clicked_label(_collector_index);
+    _graph->on_click_label(_collector_index);
     return 0;
     return 0;
 
 
   case WM_MOUSEMOVE:
   case WM_MOUSEMOVE:
     {
     {
       // When the mouse enters the label area, highlight the label.
       // When the mouse enters the label area, highlight the label.
-      set_mouse_within(true);
+      if (!_mouse_within) {
+        set_mouse_within(true);
+        _graph->on_enter_label(_collector_index);
+      }
 
 
       // Now we want to get a WM_MOUSELEAVE when the mouse leaves the label.
       // Now we want to get a WM_MOUSELEAVE when the mouse leaves the label.
       TRACKMOUSEEVENT tme = {
       TRACKMOUSEEVENT tme = {
@@ -294,7 +302,10 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     break;
     break;
 
 
   case WM_MOUSELEAVE:
   case WM_MOUSELEAVE:
-    set_mouse_within(false);
+    if (_mouse_within) {
+      set_mouse_within(false);
+      _graph->on_leave_label(_collector_index);
+    }
     break;
     break;
 
 
   case WM_PAINT:
   case WM_PAINT:
@@ -303,26 +314,33 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
       HDC hdc = BeginPaint(hwnd, &ps);
       HDC hdc = BeginPaint(hwnd, &ps);
 
 
       RECT rect = { 0, 0, _width, _height };
       RECT rect = { 0, 0, _width, _height };
-      FillRect(hdc, &rect, _bg_brush);
-
-      if (_highlight || _mouse_within) {
-        FrameRect(hdc, &rect, _highlight_brush);
-      }
+      FillRect(hdc, &rect, (_highlight || _mouse_within) ? _highlight_bg_brush : _bg_brush);
 
 
-      HFONT hfnt = (HFONT)GetStockObject(ANSI_VAR_FONT);
+      HFONT hfnt = _monitor->get_font();
       SelectObject(hdc, hfnt);
       SelectObject(hdc, hfnt);
-      SetTextAlign(hdc, TA_RIGHT | TA_TOP);
+      SetTextAlign(hdc, (_align_right ? TA_RIGHT : TA_LEFT) | TA_TOP);
 
 
-      SetBkColor(hdc, _bg_color);
-      SetBkMode(hdc, OPAQUE);
-      SetTextColor(hdc, _fg_color);
+      SetBkMode(hdc, TRANSPARENT);
+      SetTextColor(hdc, (_highlight || _mouse_within) ? _highlight_fg_color : _fg_color);
 
 
-      TextOut(hdc, _width - _right_margin, _top_margin,
-              _text.data(), _text.length());
+      TextOut(hdc, _align_right ? (_width - _right_margin) : _left_margin,
+              _top_margin, _text.data(), _text.length());
       EndPaint(hwnd, &ps);
       EndPaint(hwnd, &ps);
       return 0;
       return 0;
     }
     }
 
 
+  case WM_NOTIFY:
+    switch (((LPNMHDR)lparam)->code) {
+    case TTN_GETDISPINFO:
+      {
+        NMTTDISPINFO &info = *(NMTTDISPINFO *)lparam;
+        _tooltip_text = _graph->get_label_tooltip(_collector_index);
+        info.lpszText = (char *)_tooltip_text.c_str();
+      }
+      return 0;
+    }
+    break;
+
   default:
   default:
     break;
     break;
   }
   }

+ 18 - 10
pandatool/src/win-stats/winStatsLabel.h

@@ -32,22 +32,25 @@ class WinStatsGraph;
 class WinStatsLabel {
 class WinStatsLabel {
 public:
 public:
   WinStatsLabel(WinStatsMonitor *monitor, WinStatsGraph *graph,
   WinStatsLabel(WinStatsMonitor *monitor, WinStatsGraph *graph,
-                int thread_index, int collector_index, bool use_fullname);
+                int thread_index, int collector_index, bool use_fullname,
+                bool align_right = true);
   ~WinStatsLabel();
   ~WinStatsLabel();
 
 
   void setup(HWND parent_window);
   void setup(HWND parent_window);
   void set_pos(int x, int y, int width);
   void set_pos(int x, int y, int width);
 
 
-  int get_x() const;
-  int get_y() const;
-  int get_width() const;
-  int get_height() const;
-  int get_ideal_width() const;
+  INLINE int get_x() const;
+  INLINE int get_y() const;
+  INLINE int get_width() const;
+  INLINE int get_height() const;
+  INLINE int get_ideal_width() const;
 
 
-  int get_collector_index() const;
+  INLINE int get_collector_index() const;
 
 
   void set_highlight(bool highlight);
   void set_highlight(bool highlight);
-  bool get_highlight() const;
+  INLINE bool get_highlight() const;
+
+  void update_text(bool use_fullname);
 
 
 private:
 private:
   void set_mouse_within(bool mouse_within);
   void set_mouse_within(bool mouse_within);
@@ -63,11 +66,13 @@ private:
   int _thread_index;
   int _thread_index;
   int _collector_index;
   int _collector_index;
   std::string _text;
   std::string _text;
+  std::string _tooltip_text;
   HWND _window;
   HWND _window;
-  COLORREF _bg_color;
+  HWND _tooltip_window;
   COLORREF _fg_color;
   COLORREF _fg_color;
+  COLORREF _highlight_fg_color;
   HBRUSH _bg_brush;
   HBRUSH _bg_brush;
-  HBRUSH _highlight_brush;
+  HBRUSH _highlight_bg_brush;
 
 
   int _x;
   int _x;
   int _y;
   int _y;
@@ -76,6 +81,7 @@ private:
   int _ideal_width;
   int _ideal_width;
   bool _highlight;
   bool _highlight;
   bool _mouse_within;
   bool _mouse_within;
+  bool _align_right;
 
 
   static int _left_margin, _right_margin;
   static int _left_margin, _right_margin;
   static int _top_margin, _bottom_margin;
   static int _top_margin, _bottom_margin;
@@ -84,4 +90,6 @@ private:
   static const char * const _window_class_name;
   static const char * const _window_class_name;
 };
 };
 
 
+#include "winStatsLabel.I"
+
 #endif
 #endif

+ 85 - 2
pandatool/src/win-stats/winStatsLabelStack.cxx

@@ -200,6 +200,90 @@ add_label(WinStatsMonitor *monitor, WinStatsGraph *graph,
   return label_index;
   return label_index;
 }
 }
 
 
+/**
+ * Replaces the labels with the given collector indices.
+ */
+void WinStatsLabelStack::
+replace_labels(WinStatsMonitor *monitor, WinStatsGraph *graph,
+               int thread_index, const vector_int &collector_indices,
+               bool use_fullname) {
+
+  _ideal_width = 0;
+
+  // First skip the part of the stack that hasn't changed.
+  size_t li = 0;
+  size_t ci = 0;
+  while (ci < collector_indices.size() && li < _labels.size()) {
+    WinStatsLabel *label = _labels[li];
+    if (collector_indices[ci] != label->get_collector_index()) {
+      // Mismatch.
+      break;
+    }
+    _ideal_width = std::max(_ideal_width, label->get_ideal_width());
+    ++ci;
+    ++li;
+  }
+
+  if (ci == collector_indices.size()) {
+    if (ci == _labels.size()) {
+      // Perfect, nothing changed.
+      return;
+    }
+
+    // Simple case, just delete the rest.
+    while (li < _labels.size()) {
+      delete _labels[li++];
+    }
+    _labels.resize(ci);
+    return;
+  }
+
+  int yp = _height;
+  if (li > 0) {
+    WinStatsLabel *label = _labels[li - 1];
+    yp = label->get_y() - label->get_height();
+  }
+
+  // Make a map of remaining labels.
+  std::map<int, WinStatsLabel *> label_map;
+  for (size_t li2 = li; li2 < _labels.size(); ++li2) {
+    WinStatsLabel *label = _labels[li2];
+    label_map[label->get_collector_index()] = label;
+  }
+
+  _labels.resize(collector_indices.size());
+
+  while (ci < collector_indices.size()) {
+    int collector_index = collector_indices[ci++];
+
+    WinStatsLabel *label;
+    auto it = label_map.find(collector_index);
+    if (it == label_map.end()) {
+      // It's not in the map.  Create a new label.
+      label = new WinStatsLabel(monitor, graph, thread_index, collector_index, use_fullname);
+      if (_window) {
+        label->setup(_window);
+      }
+    } else {
+      // Erase it from the map, so that it's not deleted.
+      label = it->second;
+      label_map.erase(it);
+    }
+    if (_window) {
+      label->set_pos(0, yp, _width);
+    }
+    _ideal_width = std::max(_ideal_width, label->get_ideal_width());
+    yp -= label->get_height();
+
+    _labels[li++] = label;
+  }
+
+  // Anything that's remaining in the label map should be deleted.
+  for (auto it = label_map.begin(); it != label_map.end(); ++it) {
+    delete it->second;
+  }
+}
+
 /**
 /**
  * Returns the number of labels in the stack.
  * Returns the number of labels in the stack.
  */
  */
@@ -225,7 +309,6 @@ highlight_label(int collector_index) {
   }
   }
 }
 }
 
 
-
 /**
 /**
  * Creates the window for this stack.
  * Creates the window for this stack.
  */
  */
@@ -306,7 +389,7 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
       HDC hdc = BeginPaint(hwnd, &ps);
       HDC hdc = BeginPaint(hwnd, &ps);
 
 
       RECT rect = { 0, 0, _width, _height };
       RECT rect = { 0, 0, _width, _height };
-      FillRect(hdc, &rect, (HBRUSH)COLOR_BACKGROUND);
+      FillRect(hdc, &rect, (HBRUSH)COLOR_WINDOW);
       EndPaint(hwnd, &ps);
       EndPaint(hwnd, &ps);
       return 0;
       return 0;
     }
     }

+ 4 - 0
pandatool/src/win-stats/winStatsLabelStack.h

@@ -16,6 +16,7 @@
 
 
 #include "pandatoolbase.h"
 #include "pandatoolbase.h"
 #include "pvector.h"
 #include "pvector.h"
+#include "vector_int.h"
 
 
 #ifndef WIN32_LEAN_AND_MEAN
 #ifndef WIN32_LEAN_AND_MEAN
 #define WIN32_LEAN_AND_MEAN 1
 #define WIN32_LEAN_AND_MEAN 1
@@ -51,6 +52,9 @@ public:
   void clear_labels();
   void clear_labels();
   int add_label(WinStatsMonitor *monitor, WinStatsGraph *graph,
   int add_label(WinStatsMonitor *monitor, WinStatsGraph *graph,
                 int thread_index, int collector_index, bool use_fullname);
                 int thread_index, int collector_index, bool use_fullname);
+  void replace_labels(WinStatsMonitor *monitor, WinStatsGraph *graph,
+                      int thread_index, const vector_int &collector_indices,
+                      bool use_fullname);
   int get_num_labels() const;
   int get_num_labels() const;
 
 
   void highlight_label(int collector_index);
   void highlight_label(int collector_index);

+ 56 - 3
pandatool/src/win-stats/winStatsMonitor.cxx

@@ -15,6 +15,7 @@
 #include "winStatsServer.h"
 #include "winStatsServer.h"
 #include "winStatsStripChart.h"
 #include "winStatsStripChart.h"
 #include "winStatsPianoRoll.h"
 #include "winStatsPianoRoll.h"
+#include "winStatsFlameGraph.h"
 #include "winStatsChartMenu.h"
 #include "winStatsChartMenu.h"
 #include "winStatsMenuId.h"
 #include "winStatsMenuId.h"
 #include "pStatGraph.h"
 #include "pStatGraph.h"
@@ -37,6 +38,25 @@ WinStatsMonitor(WinStatsServer *server) : PStatMonitor(server) {
   _time_units = 0;
   _time_units = 0;
   _scroll_speed = 0.0;
   _scroll_speed = 0.0;
   _pause = false;
   _pause = false;
+
+  // Create the fonts used for rendering the UI.
+  NONCLIENTMETRICS metrics = {0};
+  metrics.cbSize = sizeof(NONCLIENTMETRICS);
+  if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &metrics, 0)) {
+    _font = CreateFontIndirect(&metrics.lfMenuFont);
+  } else {
+    _font = (HFONT)GetStockObject(ANSI_VAR_FONT);
+  }
+
+  HDC dc = GetDC(nullptr);
+  _pixel_scale = 0;
+  if (dc) {
+    _pixel_scale = GetDeviceCaps(dc, LOGPIXELSX) / (96 / 4);
+  }
+  if (_pixel_scale <= 0) {
+    _pixel_scale = 4;
+  }
+  ReleaseDC(nullptr, dc);
 }
 }
 
 
 /**
 /**
@@ -252,6 +272,22 @@ get_window() const {
   return _window;
   return _window;
 }
 }
 
 
+/**
+ * Returns the font that should be used for rendering text.
+ */
+HFONT WinStatsMonitor::
+get_font() const {
+  return _font;
+}
+
+/**
+ * Returns the system DPI scaling as a fraction where 4 = no scaling.
+ */
+int WinStatsMonitor::
+get_pixel_scale() const {
+  return _pixel_scale;
+}
+
 /**
 /**
  * Opens a new strip chart showing the indicated data.
  * Opens a new strip chart showing the indicated data.
  */
  */
@@ -279,6 +315,19 @@ open_piano_roll(int thread_index) {
   graph->set_pause(_pause);
   graph->set_pause(_pause);
 }
 }
 
 
+/**
+ * Opens a new flame graph showing the indicated data.
+ */
+void WinStatsMonitor::
+open_flame_graph(int thread_index) {
+  WinStatsFlameGraph *graph = new WinStatsFlameGraph(this, thread_index);
+  add_graph(graph);
+
+  graph->set_time_units(_time_units);
+  graph->set_scroll_speed(_scroll_speed);
+  graph->set_pause(_pause);
+}
+
 /**
 /**
  * Returns the MenuDef properties associated with the indicated menu ID.  This
  * Returns the MenuDef properties associated with the indicated menu ID.  This
  * specifies what we expect to do when the given menu has been selected.
  * specifies what we expect to do when the given menu has been selected.
@@ -604,7 +653,7 @@ register_window_class(HINSTANCE application) {
   wc.lpfnWndProc = (WNDPROC)static_window_proc;
   wc.lpfnWndProc = (WNDPROC)static_window_proc;
   wc.hInstance = application;
   wc.hInstance = application;
   wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
   wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
-  wc.hbrBackground = (HBRUSH)COLOR_BACKGROUND;
+  wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
   wc.lpszMenuName = nullptr;
   wc.lpszMenuName = nullptr;
   wc.lpszClassName = _window_class_name;
   wc.lpszClassName = _window_class_name;
 
 
@@ -701,9 +750,13 @@ handle_menu_command(int menu_id) {
   default:
   default:
     if (menu_id >= MI_new_chart) {
     if (menu_id >= MI_new_chart) {
       const MenuDef &menu_def = lookup_menu(menu_id);
       const MenuDef &menu_def = lookup_menu(menu_id);
-      if (menu_def._collector_index < 0) {
+      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);
         open_piano_roll(menu_def._thread_index);
-      } else {
+      }
+      else {
         open_strip_chart(menu_def._thread_index, menu_def._collector_index,
         open_strip_chart(menu_def._thread_index, menu_def._collector_index,
                          menu_def._show_level);
                          menu_def._show_level);
       }
       }

+ 7 - 0
pandatool/src/win-stats/winStatsMonitor.h

@@ -66,8 +66,12 @@ public:
   virtual void user_guide_bars_changed();
   virtual void user_guide_bars_changed();
 
 
   HWND get_window() const;
   HWND get_window() const;
+  HFONT get_font() const;
+  int get_pixel_scale() const;
+
   void open_strip_chart(int thread_index, int collector_index, bool show_level);
   void open_strip_chart(int thread_index, int collector_index, bool show_level);
   void open_piano_roll(int thread_index);
   void open_piano_roll(int thread_index);
+  void open_flame_graph(int thread_index);
 
 
   const MenuDef &lookup_menu(int menu_id) const;
   const MenuDef &lookup_menu(int menu_id) const;
   int get_menu_id(const MenuDef &menu_def);
   int get_menu_id(const MenuDef &menu_def);
@@ -109,6 +113,9 @@ private:
   int _time_units;
   int _time_units;
   double _scroll_speed;
   double _scroll_speed;
   bool _pause;
   bool _pause;
+  int _pixel_scale;
+
+  HFONT _font;
 
 
   static bool _window_class_registered;
   static bool _window_class_registered;
   static const char * const _window_class_name;
   static const char * const _window_class_name;

+ 47 - 30
pandatool/src/win-stats/winStatsPianoRoll.cxx

@@ -15,7 +15,7 @@
 #include "winStatsMonitor.h"
 #include "winStatsMonitor.h"
 #include "numeric_types.h"
 #include "numeric_types.h"
 
 
-static const int default_piano_roll_width = 400;
+static const int default_piano_roll_width = 600;
 static const int default_piano_roll_height = 200;
 static const int default_piano_roll_height = 200;
 
 
 bool WinStatsPianoRoll::_window_class_registered = false;
 bool WinStatsPianoRoll::_window_class_registered = false;
@@ -27,14 +27,14 @@ const char * const WinStatsPianoRoll::_window_class_name = "piano";
 WinStatsPianoRoll::
 WinStatsPianoRoll::
 WinStatsPianoRoll(WinStatsMonitor *monitor, int thread_index) :
 WinStatsPianoRoll(WinStatsMonitor *monitor, int thread_index) :
   PStatPianoRoll(monitor, thread_index,
   PStatPianoRoll(monitor, thread_index,
-                 default_piano_roll_width,
-                 default_piano_roll_height),
+                 monitor->get_pixel_scale() * default_piano_roll_width / 4,
+                 monitor->get_pixel_scale() * default_piano_roll_height / 4),
   WinStatsGraph(monitor)
   WinStatsGraph(monitor)
 {
 {
-  _left_margin = 128;
-  _right_margin = 8;
-  _top_margin = 16;
-  _bottom_margin = 8;
+  _left_margin = _pixel_scale * 32;
+  _right_margin = _pixel_scale * 2;
+  _top_margin = _pixel_scale * 5;
+  _bottom_margin = _pixel_scale * 2;
 
 
   // Let's show the units on the guide bar labels.  There's room.
   // Let's show the units on the guide bar labels.  There's room.
   set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
   set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
@@ -103,7 +103,7 @@ set_time_units(int unit_mask) {
  * Called when the user single-clicks on a label.
  * Called when the user single-clicks on a label.
  */
  */
 void WinStatsPianoRoll::
 void WinStatsPianoRoll::
-clicked_label(int collector_index) {
+on_click_label(int collector_index) {
   if (collector_index >= 0) {
   if (collector_index >= 0) {
     WinStatsGraph::_monitor->open_strip_chart(_thread_index, collector_index, false);
     WinStatsGraph::_monitor->open_strip_chart(_thread_index, collector_index, false);
   }
   }
@@ -123,6 +123,15 @@ set_horizontal_scale(double time_width) {
   InvalidateRect(_window, &rect, TRUE);
   InvalidateRect(_window, &rect, TRUE);
 }
 }
 
 
+/**
+ * Calls update_guide_bars with parameters suitable to this kind of graph.
+ */
+void WinStatsPianoRoll::
+normal_guide_bars() {
+  // We want vaguely 100 pixels between guide bars.
+  update_guide_bars(get_xsize() / (_pixel_scale * 25), get_horizontal_scale());
+}
+
 /**
 /**
  * Erases the chart area.
  * Erases the chart area.
  */
  */
@@ -144,6 +153,21 @@ begin_draw() {
   for (int i = 0; i < num_guide_bars; i++) {
   for (int i = 0; i < num_guide_bars; i++) {
     draw_guide_bar(_bitmap_dc, get_guide_bar(i));
     draw_guide_bar(_bitmap_dc, get_guide_bar(i));
   }
   }
+
+  SelectObject(_bitmap_dc, GetStockObject(NULL_PEN));
+}
+
+/**
+ * Should be overridden by the user class.  This hook will be called before
+ * drawing any one row of bars.  These bars correspond to the collector whose
+ * index is get_row_collector(row), and in the color get_row_color(row).
+ */
+void WinStatsPianoRoll::
+begin_row(int row) {
+  int collector_index = get_label_collector(row);
+  HBRUSH brush = get_collector_brush(collector_index, _highlighted_index == collector_index);
+  SelectObject(_bitmap_dc, brush);
+  SelectObject(_bitmap_dc, GetStockObject(NULL_PEN));
 }
 }
 
 
 /**
 /**
@@ -155,13 +179,7 @@ draw_bar(int row, int from_x, int to_x) {
     int y = _label_stack.get_label_y(row) - _graph_top;
     int y = _label_stack.get_label_y(row) - _graph_top;
     int height = _label_stack.get_label_height(row);
     int height = _label_stack.get_label_height(row);
 
 
-    RECT rect = {
-      from_x, y - height + 2,
-      to_x, y - 2,
-    };
-    int collector_index = get_label_collector(row);
-    HBRUSH brush = get_collector_brush(collector_index);
-    FillRect(_bitmap_dc, &rect, brush);
+    RoundRect(_bitmap_dc, from_x, y - height + 2, to_x, y - 2, _pixel_scale, _pixel_scale);
   }
   }
 }
 }
 
 
@@ -233,7 +251,10 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
       // When the mouse is over a color bar, highlight it.
       // When the mouse is over a color bar, highlight it.
       int16_t x = LOWORD(lparam);
       int16_t x = LOWORD(lparam);
       int16_t y = HIWORD(lparam);
       int16_t y = HIWORD(lparam);
-      _label_stack.highlight_label(get_collector_under_pixel(x, y));
+
+      int collector_index = get_collector_under_pixel(x, y);
+      _label_stack.highlight_label(collector_index);
+      on_enter_label(collector_index);
 
 
       // Now we want to get a WM_MOUSELEAVE when the mouse leaves the graph
       // Now we want to get a WM_MOUSELEAVE when the mouse leaves the graph
       // window.
       // window.
@@ -244,10 +265,11 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
         0
         0
       };
       };
       TrackMouseEvent(&tme);
       TrackMouseEvent(&tme);
-
-    } else {
+    }
+    else {
       // If the mouse is in some drag mode, stop highlighting.
       // If the mouse is in some drag mode, stop highlighting.
       _label_stack.highlight_label(-1);
       _label_stack.highlight_label(-1);
+      on_leave_label(_highlighted_index);
     }
     }
 
 
     if (_drag_mode == DM_scale) {
     if (_drag_mode == DM_scale) {
@@ -278,6 +300,7 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
   case WM_MOUSELEAVE:
   case WM_MOUSELEAVE:
     // When the mouse leaves the graph, stop highlighting.
     // When the mouse leaves the graph, stop highlighting.
     _label_stack.highlight_label(-1);
     _label_stack.highlight_label(-1);
+    on_leave_label(_highlighted_index);
     break;
     break;
 
 
   case WM_LBUTTONUP:
   case WM_LBUTTONUP:
@@ -305,7 +328,7 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
       // clicking on the corresponding label.
       // clicking on the corresponding label.
       int16_t x = LOWORD(lparam);
       int16_t x = LOWORD(lparam);
       int16_t y = HIWORD(lparam);
       int16_t y = HIWORD(lparam);
-      clicked_label(get_collector_under_pixel(x, y));
+      on_click_label(get_collector_under_pixel(x, y));
       return 0;
       return 0;
     }
     }
     break;
     break;
@@ -325,12 +348,11 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
 void WinStatsPianoRoll::
 void WinStatsPianoRoll::
 additional_window_paint(HDC hdc) {
 additional_window_paint(HDC hdc) {
   // Draw in the labels for the guide bars.
   // Draw in the labels for the guide bars.
-  HFONT hfnt = (HFONT)GetStockObject(ANSI_VAR_FONT);
-  SelectObject(hdc, hfnt);
+  SelectObject(hdc, WinStatsGraph::_monitor->get_font());
   SetTextAlign(hdc, TA_LEFT | TA_BOTTOM);
   SetTextAlign(hdc, TA_LEFT | TA_BOTTOM);
   SetBkMode(hdc, TRANSPARENT);
   SetBkMode(hdc, TRANSPARENT);
 
 
-  int y = _top_margin;
+  int y = _top_margin - 2;
 
 
   int i;
   int i;
   int num_guide_bars = get_num_guide_bars();
   int num_guide_bars = get_num_guide_bars();
@@ -411,13 +433,8 @@ get_collector_under_pixel(int xpoint, int ypoint) {
  */
  */
 void WinStatsPianoRoll::
 void WinStatsPianoRoll::
 update_labels() {
 update_labels() {
-  _label_stack.clear_labels();
-  for (int i = 0; i < get_num_labels(); i++) {
-    int label_index =
-      _label_stack.add_label(WinStatsGraph::_monitor, this,
-                             _thread_index,
-                             get_label_collector(i), true);
-  }
+  _label_stack.replace_labels(WinStatsGraph::_monitor, this,
+                              _thread_index, _labels, true);
   _labels_changed = false;
   _labels_changed = false;
 }
 }
 
 
@@ -551,7 +568,7 @@ register_window_class(HINSTANCE application) {
   wc.lpfnWndProc = (WNDPROC)static_window_proc;
   wc.lpfnWndProc = (WNDPROC)static_window_proc;
   wc.hInstance = application;
   wc.hInstance = application;
   wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
   wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
-  wc.hbrBackground = (HBRUSH)COLOR_BACKGROUND;
+  wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
   wc.lpszMenuName = nullptr;
   wc.lpszMenuName = nullptr;
   wc.lpszClassName = _window_class_name;
   wc.lpszClassName = _window_class_name;
 
 

+ 3 - 1
pandatool/src/win-stats/winStatsPianoRoll.h

@@ -41,12 +41,14 @@ public:
   virtual void changed_graph_size(int graph_xsize, int graph_ysize);
   virtual void changed_graph_size(int graph_xsize, int graph_ysize);
 
 
   virtual void set_time_units(int unit_mask);
   virtual void set_time_units(int unit_mask);
-  virtual void clicked_label(int collector_index);
+  virtual void on_click_label(int collector_index);
   void set_horizontal_scale(double time_width);
   void set_horizontal_scale(double time_width);
 
 
 protected:
 protected:
+  virtual void normal_guide_bars();
   void clear_region();
   void clear_region();
   virtual void begin_draw();
   virtual void begin_draw();
+  virtual void begin_row(int row);
   virtual void draw_bar(int row, int from_x, int to_x);
   virtual void draw_bar(int row, int from_x, int to_x);
   virtual void end_draw();
   virtual void end_draw();
   virtual void idle();
   virtual void idle();

+ 48 - 41
pandatool/src/win-stats/winStatsStripChart.cxx

@@ -16,17 +16,13 @@
 #include "pStatCollectorDef.h"
 #include "pStatCollectorDef.h"
 #include "numeric_types.h"
 #include "numeric_types.h"
 
 
+#include <commctrl.h>
+
 using std::string;
 using std::string;
 
 
 static const int default_strip_chart_width = 400;
 static const int default_strip_chart_width = 400;
 static const int default_strip_chart_height = 100;
 static const int default_strip_chart_height = 100;
 
 
-// Surely we aren't expected to hardcode the size of a normal checkbox.  But
-// Windows seems to require this data to be passed to CreateWindow(), so what
-// else can I do?
-size_t WinStatsStripChart::_check_box_height = 13;
-size_t WinStatsStripChart::_check_box_width = 13;
-
 bool WinStatsStripChart::_window_class_registered = false;
 bool WinStatsStripChart::_window_class_registered = false;
 const char * const WinStatsStripChart::_window_class_name = "strip";
 const char * const WinStatsStripChart::_window_class_name = "strip";
 
 
@@ -40,16 +36,16 @@ WinStatsStripChart(WinStatsMonitor *monitor, int thread_index,
                   show_level ? monitor->get_level_view(collector_index, thread_index) : monitor->get_view(thread_index),
                   show_level ? monitor->get_level_view(collector_index, thread_index) : monitor->get_view(thread_index),
                   thread_index,
                   thread_index,
                   collector_index,
                   collector_index,
-                  default_strip_chart_width,
-                  default_strip_chart_height),
+                  monitor->get_pixel_scale() * default_strip_chart_width / 4,
+                  monitor->get_pixel_scale() * default_strip_chart_height / 4),
   WinStatsGraph(monitor)
   WinStatsGraph(monitor)
 {
 {
   _brush_origin = 0;
   _brush_origin = 0;
 
 
-  _left_margin = 96;
-  _right_margin = 32;
-  _top_margin = 16;
-  _bottom_margin = 8;
+  _left_margin = _pixel_scale * 24;
+  _right_margin = _pixel_scale * 12;
+  _top_margin = _pixel_scale * 6;
+  _bottom_margin = _pixel_scale * 2;
 
 
   if (show_level) {
   if (show_level) {
     // If it's a level-type graph, show the appropriate units.
     // If it's a level-type graph, show the appropriate units.
@@ -170,7 +166,7 @@ set_scroll_speed(double scroll_speed) {
  * Called when the user single-clicks on a label.
  * Called when the user single-clicks on a label.
  */
  */
 void WinStatsStripChart::
 void WinStatsStripChart::
-clicked_label(int collector_index) {
+on_click_label(int collector_index) {
   if (collector_index < 0) {
   if (collector_index < 0) {
     // Clicking on whitespace in the graph is the same as clicking on the top
     // Clicking on whitespace in the graph is the same as clicking on the top
     // label.
     // label.
@@ -197,6 +193,15 @@ clicked_label(int collector_index) {
   }
   }
 }
 }
 
 
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string WinStatsStripChart::
+get_label_tooltip(int collector_index) const {
+  return PStatStripChart::get_label_tooltip(collector_index);
+}
+
 /**
 /**
  * Changes the value the height of the vertical axis represents.  This may
  * Changes the value the height of the vertical axis represents.  This may
  * force a redraw.
  * force a redraw.
@@ -218,11 +223,8 @@ void WinStatsStripChart::
 update_labels() {
 update_labels() {
   PStatStripChart::update_labels();
   PStatStripChart::update_labels();
 
 
-  _label_stack.clear_labels();
-  for (int i = 0; i < get_num_labels(); i++) {
-    _label_stack.add_label(WinStatsGraph::_monitor, this, _thread_index,
-                           get_label_collector(i), false);
-  }
+  _label_stack.replace_labels(WinStatsGraph::_monitor, this,
+                              _thread_index, _labels, false);
   _labels_changed = false;
   _labels_changed = false;
 }
 }
 
 
@@ -273,7 +275,7 @@ draw_slice(int x, int w, const PStatStripChart::FrameData &fdata) {
   for (fi = fdata.begin(); fi != fdata.end(); ++fi) {
   for (fi = fdata.begin(); fi != fdata.end(); ++fi) {
     const ColorData &cd = (*fi);
     const ColorData &cd = (*fi);
     overall_time += cd._net_value;
     overall_time += cd._net_value;
-    HBRUSH brush = get_collector_brush(cd._collector_index);
+    HBRUSH brush = get_collector_brush(cd._collector_index, cd._collector_index == _highlighted_index);
 
 
     if (overall_time > get_vertical_scale()) {
     if (overall_time > get_vertical_scale()) {
       // Off the top.  Go ahead and clamp it by hand, in case it's so far off
       // Off the top.  Go ahead and clamp it by hand, in case it's so far off
@@ -391,7 +393,10 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
       // When the mouse is over a color bar, highlight it.
       // When the mouse is over a color bar, highlight it.
       int16_t x = LOWORD(lparam);
       int16_t x = LOWORD(lparam);
       int16_t y = HIWORD(lparam);
       int16_t y = HIWORD(lparam);
-      _label_stack.highlight_label(get_collector_under_pixel(x, y));
+
+      int collector_index = get_collector_under_pixel(x, y);
+      _label_stack.highlight_label(collector_index);
+      on_enter_label(collector_index);
 
 
       // Now we want to get a WM_MOUSELEAVE when the mouse leaves the graph
       // Now we want to get a WM_MOUSELEAVE when the mouse leaves the graph
       // window.
       // window.
@@ -402,10 +407,11 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
         0
         0
       };
       };
       TrackMouseEvent(&tme);
       TrackMouseEvent(&tme);
-
-    } else {
+    }
+    else {
       // If the mouse is in some drag mode, stop highlighting.
       // If the mouse is in some drag mode, stop highlighting.
       _label_stack.highlight_label(-1);
       _label_stack.highlight_label(-1);
+      on_leave_label(_highlighted_index);
     }
     }
 
 
     if (_drag_mode == DM_scale) {
     if (_drag_mode == DM_scale) {
@@ -436,6 +442,7 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
   case WM_MOUSELEAVE:
   case WM_MOUSELEAVE:
     // When the mouse leaves the graph, stop highlighting.
     // When the mouse leaves the graph, stop highlighting.
     _label_stack.highlight_label(-1);
     _label_stack.highlight_label(-1);
+    on_leave_label(_highlighted_index);
     break;
     break;
 
 
   case WM_LBUTTONUP:
   case WM_LBUTTONUP:
@@ -463,7 +470,7 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
       // clicking on the corresponding label.
       // clicking on the corresponding label.
       int16_t x = LOWORD(lparam);
       int16_t x = LOWORD(lparam);
       int16_t y = HIWORD(lparam);
       int16_t y = HIWORD(lparam);
-      clicked_label(get_collector_under_pixel(x, y));
+      on_click_label(get_collector_under_pixel(x, y));
       return 0;
       return 0;
     }
     }
     break;
     break;
@@ -483,14 +490,13 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
 void WinStatsStripChart::
 void WinStatsStripChart::
 additional_window_paint(HDC hdc) {
 additional_window_paint(HDC hdc) {
   // Draw in the labels for the guide bars.
   // Draw in the labels for the guide bars.
-  HFONT hfnt = (HFONT)GetStockObject(ANSI_VAR_FONT);
-  SelectObject(hdc, hfnt);
+  SelectObject(hdc, WinStatsGraph::_monitor->get_font());
   SetTextAlign(hdc, TA_LEFT | TA_TOP);
   SetTextAlign(hdc, TA_LEFT | TA_TOP);
   SetBkMode(hdc, TRANSPARENT);
   SetBkMode(hdc, TRANSPARENT);
 
 
   RECT rect;
   RECT rect;
   GetClientRect(_window, &rect);
   GetClientRect(_window, &rect);
-  int x = rect.right - _right_margin + 2;
+  int x = rect.right - _right_margin + _pixel_scale;
   int last_y = -100;
   int last_y = -100;
 
 
   int i;
   int i;
@@ -511,15 +517,8 @@ additional_window_paint(HDC hdc) {
   // Now draw the "net value" label at the top.
   // Now draw the "net value" label at the top.
   SetTextAlign(hdc, TA_RIGHT | TA_BOTTOM);
   SetTextAlign(hdc, TA_RIGHT | TA_BOTTOM);
   SetTextColor(hdc, RGB(0, 0, 0));
   SetTextColor(hdc, RGB(0, 0, 0));
-  TextOut(hdc, rect.right - _right_margin, _top_margin,
+  TextOut(hdc, rect.right - _right_margin - _pixel_scale, _top_margin - _pixel_scale / 2,
           _net_value_text.data(), _net_value_text.length());
           _net_value_text.data(), _net_value_text.length());
-
-  // Also draw the "Smooth" label on the check box.  This isn't part of the
-  // check box itself, because doing that doesn't use the right font!  Surely
-  // this isn't the correct Windows(tm) way to do this sort of thing, but I
-  // don't know any better for now.
-  SetTextAlign(hdc, TA_LEFT | TA_BOTTOM);
-  TextOut(hdc, _left_margin + _check_box_width + 2, _top_margin, "Smooth", 6);
 }
 }
 
 
 /**
 /**
@@ -594,10 +593,13 @@ void WinStatsStripChart::
 move_graph_window(int graph_left, int graph_top, int graph_xsize, int graph_ysize) {
 move_graph_window(int graph_left, int graph_top, int graph_xsize, int graph_ysize) {
   WinStatsGraph::move_graph_window(graph_left, graph_top, graph_xsize, graph_ysize);
   WinStatsGraph::move_graph_window(graph_left, graph_top, graph_xsize, graph_ysize);
   if (_smooth_check_box != 0) {
   if (_smooth_check_box != 0) {
+    SIZE size;
+    SendMessage(_smooth_check_box, BCM_GETIDEALSIZE, 0, (LPARAM)&size);
+
     SetWindowPos(_smooth_check_box, 0,
     SetWindowPos(_smooth_check_box, 0,
-                 _left_margin, _top_margin - _check_box_height - 1,
-                 0, 0,
-                 SWP_NOZORDER | SWP_NOSIZE | SWP_SHOWWINDOW);
+                 _left_margin, _top_margin - size.cy - _pixel_scale / 2,
+                 size.cx, size.cy,
+                 SWP_NOZORDER | SWP_SHOWWINDOW);
     InvalidateRect(_smooth_check_box, nullptr, TRUE);
     InvalidateRect(_smooth_check_box, nullptr, TRUE);
   }
   }
 }
 }
@@ -715,10 +717,15 @@ create_window() {
   setup_label_stack();
   setup_label_stack();
 
 
   _smooth_check_box =
   _smooth_check_box =
-    CreateWindow("BUTTON", "",
-                 WS_CHILD | BS_AUTOCHECKBOX,
-                 0, 0, _check_box_width, _check_box_height,
+    CreateWindow(WC_BUTTON, "Smooth", WS_CHILD | BS_AUTOCHECKBOX,
+                 0, 0, 0, 0,
                  _window, nullptr, application, 0);
                  _window, nullptr, application, 0);
+  SendMessage(_smooth_check_box, WM_SETFONT,
+              (WPARAM)WinStatsGraph::_monitor->get_font(), TRUE);
+
+  if (get_average_mode()) {
+    SendMessage(_smooth_check_box, BM_SETCHECK, BST_CHECKED, 0);
+  }
 
 
   // Ensure that the window is on top of the stack.
   // Ensure that the window is on top of the stack.
   SetWindowPos(_window, HWND_TOP, 0, 0, 0, 0,
   SetWindowPos(_window, HWND_TOP, 0, 0, 0, 0,
@@ -742,7 +749,7 @@ register_window_class(HINSTANCE application) {
   wc.lpfnWndProc = (WNDPROC)static_window_proc;
   wc.lpfnWndProc = (WNDPROC)static_window_proc;
   wc.hInstance = application;
   wc.hInstance = application;
   wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
   wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
-  wc.hbrBackground = (HBRUSH)COLOR_BACKGROUND;
+  wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
   wc.lpszMenuName = nullptr;
   wc.lpszMenuName = nullptr;
   wc.lpszClassName = _window_class_name;
   wc.lpszClassName = _window_class_name;
 
 

+ 2 - 2
pandatool/src/win-stats/winStatsStripChart.h

@@ -43,7 +43,8 @@ public:
 
 
   virtual void set_time_units(int unit_mask);
   virtual void set_time_units(int unit_mask);
   virtual void set_scroll_speed(double scroll_speed);
   virtual void set_scroll_speed(double scroll_speed);
-  virtual void clicked_label(int collector_index);
+  virtual void on_click_label(int collector_index);
+  virtual std::string get_label_tooltip(int collector_index) const;
   void set_vertical_scale(double value_height);
   void set_vertical_scale(double value_height);
 
 
 protected:
 protected:
@@ -79,7 +80,6 @@ private:
   std::string _net_value_text;
   std::string _net_value_text;
 
 
   HWND _smooth_check_box;
   HWND _smooth_check_box;
-  static size_t _check_box_height, _check_box_width;
 
 
   static bool _window_class_registered;
   static bool _window_class_registered;
   static const char * const _window_class_name;
   static const char * const _window_class_name;

+ 1 - 0
pandatool/src/win-stats/winstats_composite1.cxx

@@ -1,5 +1,6 @@
 #include "winStats.cxx"
 #include "winStats.cxx"
 #include "winStatsChartMenu.cxx"
 #include "winStatsChartMenu.cxx"
+#include "winStatsFlameGraph.cxx"
 #include "winStatsGraph.cxx"
 #include "winStatsGraph.cxx"
 #include "winStatsLabel.cxx"
 #include "winStatsLabel.cxx"
 #include "winStatsLabelStack.cxx"
 #include "winStatsLabelStack.cxx"