Major improvement of the CPU load optimization implementation. The
CPUMonitor class is not used any more, that work is implemented in the
PollingScheduler class instead. New implementation solves the problem
of "random" sudden changes of CPU load that could be seen in previous
versions. Also, measurement method for CPU utilization has been
changed, the old one did not count CPU utilization increased by other
processes.


git-svn-id: svn://svn.code.sf.net/p/tigervnc/code/trunk@496 3789f03b-4d11-0410-bbf8-ca57d06f2519
diff --git a/x0vncserver/CPUMonitor.cxx b/x0vncserver/CPUMonitor.cxx
deleted file mode 100644
index be2214f..0000000
--- a/x0vncserver/CPUMonitor.cxx
+++ /dev/null
@@ -1,102 +0,0 @@
-/* Copyright (C) 2005 Constantin Kaplinsky.  All Rights Reserved.
- *    
- * This is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- * 
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with this software; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
- * USA.
- */
-
-//
-// CPUMonitor.cxx
-//
-
-#include <x0vncserver/CPUMonitor.h>
-
-CPUMonitor::CPUMonitor(int optimalLevel, int updatePeriod)
-{
-  setLevel(optimalLevel);
-  setPeriod(updatePeriod);
-  update();
-}
-
-void CPUMonitor::setLevel(int optimalLevel)
-{
-  m_optimalLevel = optimalLevel;
-  if (m_optimalLevel < 0) {
-    m_optimalLevel = 0;
-  } else if (m_optimalLevel > 100) {
-    m_optimalLevel = 100;
-  }
-}
-
-void CPUMonitor::setPeriod(int updatePeriod)
-{
-  m_updatePeriod = (updatePeriod > 10) ? updatePeriod : 10;
-}
-
-void CPUMonitor::update()
-{
-  getClock(&m_savedTime, &m_savedClock);
-}
-
-int CPUMonitor::check()
-{
-  TimeMillis timeNow;
-  clock_t clockNow;
-  getClock(&timeNow, &clockNow);
-
-  int coeff = 100;              // 100% means no changes.
-
-  if (m_savedClock != (clock_t)-1 && clockNow != (clock_t)-1) {
-
-    // Find out how much real time has been elapsed (in milliseconds).
-    int timeDiff = timeNow.diffFrom(m_savedTime);
-
-    if (timeDiff < m_updatePeriod) {
-      // Measuring CPU usage is problematic in this case. So return
-      // 100 and do not update saved time and clock numbers.
-      return coeff;
-    }
-
-    // Calculate how much processor time has been consumed (in milliseconds).
-    unsigned int clockDiff = (unsigned int)(clockNow - m_savedClock);
-    clockDiff /= (CLOCKS_PER_SEC / 1000);
-
-    // Get actual CPU usage and convert optimal level (to 1/1000).
-    int realUsage = (clockDiff * 1000 + timeDiff/2) / timeDiff;
-    int optimalUsage = m_optimalLevel * 10;
-
-    // Compute correction coefficient (in percents).
-    if (realUsage != 0) {
-      coeff = (optimalUsage * 100 + realUsage/2) / realUsage;
-    } else {
-      coeff = 10000;
-    }
-
-  }
-
-  // Update saved time and clock numbers.
-  m_savedTime = timeNow;
-  m_savedClock = clockNow;
-
-  return coeff;
-}
-
-void CPUMonitor::getClock(TimeMillis *tm, clock_t *clk)
-{
-  if (tm->update()) {
-    *clk = clock();
-  } else {
-    *clk = (clock_t)-1;
-  }
-}
diff --git a/x0vncserver/CPUMonitor.h b/x0vncserver/CPUMonitor.h
deleted file mode 100644
index 0421e51..0000000
--- a/x0vncserver/CPUMonitor.h
+++ /dev/null
@@ -1,78 +0,0 @@
-/* Copyright (C) 2005 Constantin Kaplinsky.  All Rights Reserved.
- *    
- * This is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- * 
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with this software; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
- * USA.
- */
-
-//
-// CPUMonitor.h
-//
-
-#ifndef __CPUMONITOR_H__
-#define __CPUMONITOR_H__
-
-#include <time.h>
-
-#include <x0vncserver/TimeMillis.h>
-
-class CPUMonitor {
-
-public:
-
-  CPUMonitor(int optimalLevel = 50, int updatePeriod = 1000);
-
-  //
-  // Optimal level is the CPU utilization level to maintain, in
-  // percents.
-  //
-  void setLevel(int optimalLevel);
-
-  //
-  // Update period is the minimum time in milliseconds that has to
-  // pass after update() or check() call, before next check() call
-  // will return meaningful value.
-  //
-  void setPeriod(int updatePeriod);
-
-  //
-  // Save current time and CPU clock, to use in the next check() call.
-  //
-  void update();
-
-  //
-  // This method calculates recent CPU utilization and returns a
-  // percentage factor which would convert current CPU usage into the
-  // optimal value. For example, if the optimal level was set to 40%
-  // and the real value was 50%, the function will return 80, because
-  // 50 * 80% == 40.
-  //
-  // If the CPU utilization cannot be measured, this function returns
-  // 100. This may be the case on the first call to check(), or if the
-  // time period between two successive check() calls was too small.
-  //
-  int check();
-
-protected:
-
-  static void getClock(TimeMillis *tm, clock_t *clk);
-
-  int m_optimalLevel;
-  int m_updatePeriod;
-  TimeMillis m_savedTime;
-  clock_t m_savedClock;
-
-};
-
-#endif // __CPUMONITOR_H__
diff --git a/x0vncserver/Makefile.in b/x0vncserver/Makefile.in
index f9a2ad7..b6bd0ba 100644
--- a/x0vncserver/Makefile.in
+++ b/x0vncserver/Makefile.in
@@ -1,6 +1,6 @@
 
-SRCS = Image.cxx CPUMonitor.cxx TimeMillis.cxx PollingManager.cxx \
-       PollingScheduler.cxx x0vncserver.cxx
+SRCS = Image.cxx TimeMillis.cxx PollingScheduler.cxx PollingManager.cxx \
+       x0vncserver.cxx
 
 OBJS = $(SRCS:.cxx=.o)
 
diff --git a/x0vncserver/PollingScheduler.cxx b/x0vncserver/PollingScheduler.cxx
index f326351..af597df 100644
--- a/x0vncserver/PollingScheduler.cxx
+++ b/x0vncserver/PollingScheduler.cxx
@@ -20,17 +20,30 @@
 // PollingScheduler class implementation.
 //
 
+#include <string.h>
+#include <stdlib.h>
+
 #include <x0vncserver/PollingScheduler.h>
 
-PollingScheduler::PollingScheduler(int interval)
+PollingScheduler::PollingScheduler(int interval, int maxload)
 {
-  setInterval(interval);
+  setParameters(interval, maxload);
   reset();
 }
 
-void PollingScheduler::setInterval(int interval)
+void PollingScheduler::setParameters(int interval, int maxload)
 {
   m_interval = interval;
+  m_maxload = maxload;
+
+  if (m_interval < 0) {
+    m_interval = 0;
+  }
+  if (m_maxload < 1) {
+    m_maxload = 1;
+  } else if (m_maxload > 100) {
+    m_maxload = 100;
+  }
 }
 
 void PollingScheduler::reset()
@@ -40,8 +53,93 @@
 
 void PollingScheduler::newPass()
 {
-  m_passStarted.update();
-  m_initialState = false;
+  TimeMillis timeNow;
+
+  if (m_initialState) {
+
+    // First polling pass: initialize statistics.
+    m_initialState = false;
+    m_ratedDuration = 0;
+    m_sleeping = 0;
+    memset(m_errors, 0, sizeof(m_errors));
+    m_errorSum = 0;
+    m_errorAbsSum = 0;
+    memset(m_durations, 0, sizeof(m_durations));
+    m_durationSum = 0;
+    memset(m_slept, 0, sizeof(m_slept));
+    m_sleptSum = 0;
+    m_idx = 0;
+
+  } else {
+
+    // Stop sleeping if not yet.
+    if (m_sleeping)
+      sleepFinished();
+
+    // Update statistics on sleeping time and total pass duration.
+    int duration = timeNow.diffFrom(m_passStarted);
+
+    int oldest = m_durations[m_idx];
+    m_durations[m_idx] = duration;
+    m_durationSum = m_durationSum - oldest + duration;
+
+    oldest = m_slept[m_idx];
+    m_slept[m_idx] = m_sleptThisPass;
+    m_sleptSum = m_sleptSum - oldest + m_sleptThisPass;
+
+    // Compute and save the difference between actual and planned time.
+    int newError = duration - m_interval;
+    oldest = m_errors[m_idx];
+    m_errors[m_idx] = newError;
+    m_errorSum = m_errorSum - oldest + newError;
+    m_errorAbsSum = m_errorAbsSum - abs(oldest) + abs(newError);
+
+    //
+    // Here is the most important part.
+    // Compute desired duration of the upcoming polling pass.
+    //
+    m_ratedDuration = m_interval - m_errorSum;
+
+    int optimalLoadDuration =
+      ((m_durationSum - m_sleptSum) * 900 + m_maxload * 4) / (m_maxload * 8)
+      - m_durationSum;
+
+    if (m_ratedDuration < optimalLoadDuration) {
+      m_ratedDuration = optimalLoadDuration;
+    }
+
+    if (m_ratedDuration < 0) {
+      m_ratedDuration = 0;
+    }
+
+    // Update ring buffer indexer (8 elements in the arrays).
+    m_idx = (m_idx + 1) & 7;
+
+  }
+
+  m_passStarted = timeNow;
+  m_sleptThisPass = 0;
+}
+
+void PollingScheduler::sleepStarted()
+{
+  if (m_initialState || m_sleeping)
+    return;
+
+  m_sleepStarted.update();
+
+  m_sleeping = true;
+}
+
+void PollingScheduler::sleepFinished()
+{
+  if (m_initialState || !m_sleeping)
+    return;
+
+  TimeMillis timeNow;
+  m_sleptThisPass += timeNow.diffFrom(m_sleepStarted);
+
+  m_sleeping = false;
 }
 
 int PollingScheduler::millisRemaining() const
@@ -52,13 +150,20 @@
   TimeMillis timeNow;
   int elapsed = timeNow.diffFrom(m_passStarted);
 
-  if (elapsed > m_interval)
+  if (elapsed > m_ratedDuration)
     return 0;
 
-  return (m_interval - elapsed);
+  return (m_ratedDuration - elapsed);
 }
 
 bool PollingScheduler::goodTimeToPoll() const
 {
-  return (millisRemaining() == 0);
+  if (m_initialState)
+    return true;
+
+  // Average error (per 8 elements in the ring buffer).
+  int errorAvg = (m_errorAbsSum + 4) / 8;
+
+  // It's ok to poll earlier if new error is no more than half-average.
+  return (millisRemaining() <= errorAvg / 2);
 }
diff --git a/x0vncserver/PollingScheduler.h b/x0vncserver/PollingScheduler.h
index 9737685..025139a 100644
--- a/x0vncserver/PollingScheduler.h
+++ b/x0vncserver/PollingScheduler.h
@@ -18,9 +18,10 @@
 
 //
 // PollingScheduler class. It is used for deciding when to start new
-// polling pass, and how much time it is ok to wait before starting. 
-// PollingScheduler is provided a desired polling interval and watches
-// for actual intervals between past polling cycles.
+// polling pass, and how much time it is ok to sleep before starting. 
+// PollingScheduler is given a desired polling interval, but it can
+// add time between polling passes if needed for satisfying processor
+// usage limitation.
 //
 
 #ifndef __POLLINGSCHEDULER_H__
@@ -32,10 +33,10 @@
 
 public:
 
-  PollingScheduler(int interval);
+  PollingScheduler(int interval, int maxload = 50);
 
-  // Set desired polling interval.
-  void setInterval(int interval);
+  // Set polling parameters.
+  void setParameters(int interval, int maxload = 50);
 
   // Reset the object into the initial state (no polling performed).
   void reset();
@@ -43,6 +44,10 @@
   // Tell the scheduler that new polling pass is just being started.
   void newPass();
 
+  // Inform the scheduler about times when we sleep.
+  void sleepStarted();
+  void sleepFinished();
+
   // This function estimates time remaining before new polling pass.
   int millisRemaining() const;
 
@@ -51,10 +56,40 @@
 
 protected:
 
-  bool m_initialState;
+  // Parameters.
   int m_interval;
+  int m_maxload;
+
+  // This boolean flag is true when we do not poll the screen.
+  bool m_initialState;
+
+  // Time stamp saved on starting current polling pass.
   TimeMillis m_passStarted;
 
+  // Desired duration of current polling pass.
+  int m_ratedDuration;
+
+  // These are for measuring sleep time in current pass.
+  TimeMillis m_sleepStarted;
+  bool m_sleeping;
+  int m_sleptThisPass;
+
+  // Ring buffer for tracking past timing errors.
+  int m_errors[8];
+  int m_errorSum;
+  int m_errorAbsSum;
+
+  // Ring buffer for tracking total pass durations (work + sleep).
+  int m_durations[8];
+  int m_durationSum;
+
+  // Ring buffer for tracking past sleep times.
+  int m_slept[8];
+  int m_sleptSum;
+
+  // Indexer for all ring buffers.
+  int m_idx;
+
 };
 
 #endif // __POLLINGSCHEDULER_H__
diff --git a/x0vncserver/x0vncserver.cxx b/x0vncserver/x0vncserver.cxx
index d179d94..30e5416 100644
--- a/x0vncserver/x0vncserver.cxx
+++ b/x0vncserver/x0vncserver.cxx
@@ -43,7 +43,6 @@
 
 #include <x0vncserver/Image.h>
 #include <x0vncserver/PollingManager.h>
-#include <x0vncserver/CPUMonitor.h>
 #include <x0vncserver/PollingScheduler.h>
 
 using namespace rfb;
@@ -324,48 +323,6 @@
   exit(1);
 }
 
-//
-// Adjust polling cycle to satisfy MaxProcessorUsage setting.
-//
-
-static void adjustPollingCycle(int *cycle, CPUMonitor *mon)
-{
-  int coeff = mon->check();
-  if (coeff < 90 || coeff > 110) {
-    int oldValue = *cycle;
-    int newValue = (oldValue * 100 + coeff/2) / coeff;
-
-    // Correct sudden changes.
-    if (newValue < oldValue / 2) {
-      newValue = oldValue / 2;
-    } else if (newValue > oldValue * 2) {
-      newValue = oldValue * 2;
-    }
-
-    // Compute upper limit.
-    int upperLimit = (int)pollingCycle * 32;
-    if (upperLimit < 100) {
-      upperLimit = 100;
-    } else if (upperLimit > 1000) {
-      upperLimit = 1000;
-    }
-
-    // Limit lower and upper bounds.
-    if (newValue < (int)pollingCycle) {
-      newValue = (int)pollingCycle;
-    } else if (newValue > upperLimit) {
-      newValue = upperLimit;
-    }
-
-    if (newValue != oldValue) {
-      *cycle = newValue;
-#ifdef DEBUG
-      fprintf(stderr, "\t[new cycle %dms]\n", newValue);
-#endif
-    }
-  }
-}
-
 int main(int argc, char** argv)
 {
   initStdIOLoggers();
@@ -415,9 +372,7 @@
     if (strlen(hostsFile.getData()) != 0)
       listener.setFilter(&fileTcpFilter);
 
-    CPUMonitor cpumon((int)maxProcessorUsage, 1000);
-    int dynPollingCycle = (int)pollingCycle;
-    PollingScheduler sched(dynPollingCycle);
+    PollingScheduler sched((int)pollingCycle, (int)maxProcessorUsage);
 
     fd_set rfds;
     struct timeval tv;
@@ -451,7 +406,10 @@
       }
       tv.tv_sec = 0;
 
+      sched.sleepStarted();
       int n = select(FD_SETSIZE, &rfds, 0, 0, &tv);
+      sched.sleepFinished();
+
       if (n < 0) {
         if (errno == EINTR) {
           vlog.debug("interrupted select() system call");
@@ -465,7 +423,6 @@
         Socket* sock = listener.accept();
         if (sock) {
           server.addClient(sock);
-          cpumon.update();      // count time from now
         } else {
           vlog.status("Client connection rejected");
         }
@@ -486,8 +443,6 @@
       server.checkTimeouts();
 
       if (sched.goodTimeToPoll()) {
-        adjustPollingCycle(&dynPollingCycle, &cpumon);
-        sched.setInterval(dynPollingCycle);
         sched.newPass();
         desktop.poll();
       }