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/CConn.cxx b/vncviewer/CConn.cxx
index a692732..addc30d 100644
--- a/vncviewer/CConn.cxx
+++ b/vncviewer/CConn.cxx
@@ -73,7 +73,7 @@
 
 CConn::CConn(const char* vncServerName, network::Socket* socket=NULL)
   : serverHost(0), serverPort(0), desktop(NULL),
-    pendingPFChange(false),
+    frameCount(0), pixelCount(0), pendingPFChange(false),
     currentEncoding(encodingTight), lastServerEncoding((unsigned int)-1),
     formatChange(false), encodingChange(false),
     firstUpdate(true), pendingUpdate(false), continuousUpdates(false),
@@ -223,6 +223,21 @@
   return infoText;
 }
 
+unsigned CConn::getFrameCount()
+{
+  return frameCount;
+}
+
+unsigned CConn::getPixelCount()
+{
+  return pixelCount;
+}
+
+unsigned CConn::getPosition()
+{
+  return sock->inStream().pos();
+}
+
 // The RFB core is not properly asynchronous, so it calls this callback
 // whenever it needs to block to wait for more data. Since FLTK is
 // monitoring the socket, we just make sure FLTK gets to run.
@@ -365,6 +380,8 @@
 {
   CConnection::framebufferUpdateEnd();
 
+  frameCount++;
+
   Fl::remove_timeout(handleUpdateTimeout, this);
   desktop->updateWindow();
 
@@ -441,6 +458,8 @@
   CConnection::dataRect(r, encoding);
 
   sock->inStream().stopTiming();
+
+  pixelCount += r.area();
 }
 
 void CConn::setCursor(int width, int height, const Point& hotspot,
diff --git a/vncviewer/CConn.h b/vncviewer/CConn.h
index d6dd4a7..93cc278 100644
--- a/vncviewer/CConn.h
+++ b/vncviewer/CConn.h
@@ -40,6 +40,10 @@
 
   const char *connectionInfo();
 
+  unsigned getFrameCount();
+  unsigned getPixelCount();
+  unsigned getPosition();
+
   // FdInStreamBlockCallback methods
   void blockCallback();
 
@@ -89,6 +93,9 @@
 
   DesktopWindow *desktop;
 
+  unsigned frameCount;
+  unsigned pixelCount;
+
   rfb::PixelFormat serverPF;
   rfb::PixelFormat fullColourPF;
 
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);
+}
diff --git a/vncviewer/DesktopWindow.h b/vncviewer/DesktopWindow.h
index 11f3dc2..4224699 100644
--- a/vncviewer/DesktopWindow.h
+++ b/vncviewer/DesktopWindow.h
@@ -22,6 +22,8 @@
 
 #include <map>
 
+#include <sys/time.h>
+
 #include <rfb/Rect.h>
 #include <rfb/Pixel.h>
 
@@ -103,6 +105,8 @@
   static void handleScroll(Fl_Widget *wnd, void *data);
   static void handleEdgeScroll(void *data);
 
+  static void handleStatsTimeout(void *data);
+
 private:
   CConn* cc;
   Fl_Scrollbar *hscroll, *vscroll;
@@ -115,6 +119,20 @@
   bool firstUpdate;
   bool delayedFullscreen;
   bool delayedDesktopSize;
+
+  struct statsEntry {
+    unsigned fps;
+    unsigned pps;
+    unsigned bps;
+  };
+  struct statsEntry stats[100];
+
+  struct timeval statsLastTime;
+  unsigned statsLastFrame;
+  unsigned statsLastPixels;
+  unsigned statsLastPosition;
+
+  Surface *statsGraph;
 };
 
 #endif