Display performance statistics in viewer

Adds an optional graph to the viewer to display current frame rate,
pixel rate and network bandwidth. Makes it easier to debug and test
performance related issues.
diff --git a/vncviewer/DesktopWindow.cxx b/vncviewer/DesktopWindow.cxx
index 8fdd59b..1f0f55f 100644
--- a/vncviewer/DesktopWindow.cxx
+++ b/vncviewer/DesktopWindow.cxx
@@ -64,7 +64,9 @@
                              CConn* cc_)
   : Fl_Window(w, h), cc(cc_), offscreen(NULL), overlay(NULL),
     firstUpdate(true),
-    delayedFullscreen(false), delayedDesktopSize(false)
+    delayedFullscreen(false), delayedDesktopSize(false),
+    statsLastFrame(0), statsLastPixels(0), statsLastPosition(0),
+    statsGraph(NULL)
 {
   Fl_Group* group;
 
@@ -174,6 +176,12 @@
     fullscreen_on();
   }
 
+  // Throughput graph for debugging
+  if (vlog.getLevel() >= LogWriter::LEVEL_DEBUG) {
+    memset(&stats, 0, sizeof(stats));
+    Fl::add_timeout(0, handleStatsTimeout, this);
+  }
+
   // Show hint about menu key
   Fl::add_timeout(0.5, menuOverlay, this);
 }
@@ -187,6 +195,7 @@
   Fl::remove_timeout(handleResizeTimeout, this);
   Fl::remove_timeout(handleFullscreenTimeout, this);
   Fl::remove_timeout(handleEdgeScroll, this);
+  Fl::remove_timeout(handleStatsTimeout, this);
   Fl::remove_timeout(menuOverlay, this);
   Fl::remove_timeout(updateOverlay, this);
 
@@ -195,6 +204,8 @@
   delete overlay;
   delete offscreen;
 
+  delete statsGraph;
+
   // FLTK automatically deletes all child widgets, so we shouldn't touch
   // them ourselves here
 }
@@ -325,6 +336,23 @@
       update_child(*viewport);
   }
 
+  // Debug graph (if active)
+  if (statsGraph) {
+    int ox, oy, ow, oh;
+
+    ox = X = w() - statsGraph->width() - 30;
+    oy = Y = h() - statsGraph->height() - 30;
+    ow = statsGraph->width();
+    oh = statsGraph->height();
+
+    fl_clip_box(ox, oy, ow, oh, ox, oy, ow, oh);
+
+    if (offscreen)
+      statsGraph->blend(offscreen, ox - X, oy - Y, ox, oy, ow, oh, 204);
+    else
+      statsGraph->blend(ox - X, oy - Y, ox, oy, ow, oh, 204);
+  }
+
   // Overlay (if active)
   if (overlay) {
     int ox, oy, ow, oh;
@@ -1161,3 +1189,128 @@
 
   Fl::repeat_timeout(0.1, handleEdgeScroll, data);
 }
+
+void DesktopWindow::handleStatsTimeout(void *data)
+{
+  DesktopWindow *self = (DesktopWindow*)data;
+
+  const size_t statsCount = sizeof(stats)/sizeof(stats[0]);
+
+  unsigned frame, pixels, pos;
+  unsigned elapsed;
+
+  const unsigned statsWidth = 200;
+  const unsigned statsHeight = 100;
+  const unsigned graphWidth = statsWidth - 10;
+  const unsigned graphHeight = statsHeight - 25;
+
+  Fl_Image_Surface *surface;
+  Fl_RGB_Image *image;
+
+  unsigned maxFPS, maxPPS, maxBPS;
+  size_t i;
+
+  char buffer[256];
+
+  frame = self->cc->getFrameCount();
+  pixels = self->cc->getPixelCount();
+  pos = self->cc->getPosition();
+  elapsed = msSince(&self->statsLastTime);
+  if (elapsed < 1)
+    elapsed = 1;
+
+  memmove(&self->stats[0], &self->stats[1], sizeof(stats[0])*(statsCount-1));
+
+  self->stats[statsCount-1].fps = (frame - self->statsLastFrame) * 1000 / elapsed;
+  self->stats[statsCount-1].pps = (pixels - self->statsLastPixels) * 1000 / elapsed;
+  self->stats[statsCount-1].bps = (pos - self->statsLastPosition) * 1000 / elapsed;
+
+  gettimeofday(&self->statsLastTime, NULL);
+  self->statsLastFrame = frame;
+  self->statsLastPixels = pixels;
+  self->statsLastPosition = pos;
+
+#if !defined(WIN32) && !defined(__APPLE__)
+  // FLTK < 1.3.5 crashes if fl_gc is unset
+  if (!fl_gc)
+    fl_gc = XDefaultGC(fl_display, 0);
+#endif
+
+  surface = new Fl_Image_Surface(statsWidth, statsHeight);
+  surface->set_current();
+
+  fl_rectf(0, 0, statsWidth, statsHeight, FL_BLACK);
+
+  fl_rect(5, 5, graphWidth, graphHeight, FL_WHITE);
+
+  maxFPS = maxPPS = maxBPS = 0;
+  for (i = 0;i < statsCount;i++) {
+    if (self->stats[i].fps > maxFPS)
+      maxFPS = self->stats[i].fps;
+    if (self->stats[i].pps > maxPPS)
+      maxPPS = self->stats[i].pps;
+    if (self->stats[i].bps > maxBPS)
+      maxBPS = self->stats[i].bps;
+  }
+
+  if (maxFPS != 0) {
+    fl_color(FL_GREEN);
+    for (i = 0;i < statsCount-1;i++) {
+      fl_line(5 + i * graphWidth / statsCount,
+              5 + graphHeight - graphHeight * self->stats[i].fps / maxFPS,
+              5 + (i+1) * graphWidth / statsCount,
+              5 + graphHeight - graphHeight * self->stats[i+1].fps / maxFPS);
+    }
+  }
+
+  if (maxPPS != 0) {
+    fl_color(FL_YELLOW);
+    for (i = 0;i < statsCount-1;i++) {
+      fl_line(5 + i * graphWidth / statsCount,
+              5 + graphHeight - graphHeight * self->stats[i].pps / maxPPS,
+              5 + (i+1) * graphWidth / statsCount,
+              5 + graphHeight - graphHeight * self->stats[i+1].pps / maxPPS);
+    }
+  }
+
+  if (maxBPS != 0) {
+    fl_color(FL_RED);
+    for (i = 0;i < statsCount-1;i++) {
+      fl_line(5 + i * graphWidth / statsCount,
+              5 + graphHeight - graphHeight * self->stats[i].bps / maxBPS,
+              5 + (i+1) * graphWidth / statsCount,
+              5 + graphHeight - graphHeight * self->stats[i+1].bps / maxBPS);
+    }
+  }
+
+  fl_font(FL_HELVETICA, 10);
+
+  fl_color(FL_GREEN);
+  snprintf(buffer, sizeof(buffer), "%u fps", self->stats[statsCount-1].fps);
+  fl_draw(buffer, 5, statsHeight - 5);
+
+  fl_color(FL_YELLOW);
+  siPrefix(self->stats[statsCount-1].pps * 8, "pix/s",
+           buffer, sizeof(buffer), 3);
+  fl_draw(buffer, 5 + (statsWidth-10)/3, statsHeight - 5);
+
+  fl_color(FL_RED);
+  iecPrefix(self->stats[statsCount-1].bps * 8, "Bps",
+            buffer, sizeof(buffer), 3);
+  fl_draw(buffer, 5 + (statsWidth-10)*2/3, statsHeight - 5);
+
+  image = surface->image();
+  delete surface;
+
+  Fl_Display_Device::display_device()->set_current();
+
+  delete self->statsGraph;
+  self->statsGraph = new Surface(image);
+  delete image;
+
+  self->damage(FL_DAMAGE_CHILD, self->w() - statsWidth - 30,
+               self->h() - statsHeight - 30,
+               statsWidth, statsHeight);
+
+  Fl::repeat_timeout(0.5, handleStatsTimeout, data);
+}