[mapguide-commits] r10220 - in branches/4.0/MgDev/Server/src: Common/Manager UnitTesting

svn_mapguide at osgeo.org svn_mapguide at osgeo.org
Wed Oct 21 01:23:19 PDT 2026


Author: jng
Date: 2026-10-21 01:23:18 -0700 (Wed, 21 Oct 2026)
New Revision: 10220

Modified:
   branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.cpp
   branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.h
   branches/4.0/MgDev/Server/src/UnitTesting/TestServerAdminService.cpp
   branches/4.0/MgDev/Server/src/UnitTesting/TestServerManager.cpp
Log:
Make reading CPU utilization numbers more robust on Linux. Fixes #1892

Modified: branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.cpp	2026-10-19 16:06:14 UTC (rev 10219)
+++ branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.cpp	2026-10-21 08:23:18 UTC (rev 10220)
@@ -29,6 +29,7 @@
 #include <sys/time.h>
 #include <sys/resource.h>
 #include <unistd.h>
+#include <cinttypes>
 #endif
 
 const STRING MgServerManager::DocumentExtension = L".awd";
@@ -1278,12 +1279,93 @@
     return nCpuLoad;
 }
 
+#ifndef _WIN32
 ///////////////////////////////////////////////////////////////////////////////////
 /// <summary>
+/// Reads the aggregate "cpu" line of /proc/stat.
+/// </summary>
+/// <remarks>
+/// The counters are cumulative jiffies since boot, summed over every CPU and
+/// expressed in USER_HZ (100 Hz), so they are read as 64-bit values. On a
+/// many-core host a 32-bit counter passes 2^31 after anything from a few weeks
+/// to a few months of uptime.
+/// </remarks>
+/// <returns>
+/// True if all four counters were parsed.
+/// </returns>
+static bool ReadLinuxCpuTimes(MgServerManager::CpuSample& sample)
+{
+    FILE* stat = fopen(MgUtil::WideCharToMultiByte(MgUtil::LinuxFilesystemStat).c_str(), "r");
+    if (NULL == stat)
+    {
+        MgStringCollection arguments;
+        arguments.Add(MgUtil::LinuxFilesystemStat);
+        throw new MgException(MgExceptionCodes::MgFileNotFoundException, L"MgServerManager.ReadLinuxCpuTimes", __LINE__, __WFILE__, &arguments, L"", NULL);
+    }
+
+    // Sized so that the line cannot be truncated as the counters grow
+    char line[256];
+    bool parsed = (NULL != fgets(line, sizeof(line), stat))
+        && (4 == sscanf(line, "%*3s %" SCNu64 " %" SCNu64 " %" SCNu64 " %" SCNu64,
+                        &sample.user, &sample.nice, &sample.sys, &sample.idle));
+
+    fclose(stat);
+
+    return parsed;
+}
+
+///////////////////////////////////////////////////////////////////////////////////
+/// <summary>
+/// Computes the CPU utilization in percent between two /proc/stat samples.
+/// </summary>
+/// <remarks>
+/// The counters are cumulative, so only their deltas are meaningful. A sample
+/// that cannot be compared must not be turned into a number: an empty interval
+/// would divide 0 by 0, and the NaN that produces converts to INT_MIN.
+/// </remarks>
+/// <returns>
+/// The utilization in percent, or -1 if the samples cannot be compared.
+/// </returns>
+INT32 MgServerManager::ComputeCpuUtilization(const CpuSample& previous, const CpuSample& current)
+{
+    // A counter that moved backwards (a CPU was hot-unplugged, for example)
+    // makes the sample meaningless.
+    if (current.user < previous.user || current.nice < previous.nice
+        || current.sys < previous.sys || current.idle < previous.idle)
+    {
+        return -1;
+    }
+
+    UINT64 total = (current.user - previous.user) + (current.nice - previous.nice)
+        + (current.sys - previous.sys) + (current.idle - previous.idle);
+
+    if (0 == total)
+    {
+        return -1;
+    }
+
+    double load = (double)(current.idle - previous.idle) / (double)total * 100;
+
+    // Keep the conversion to INT32 defined even if the counters are inconsistent
+    if (load < 0.0)
+    {
+        load = 0.0;
+    }
+    else if (load > 100.0)
+    {
+        load = 100.0;
+    }
+
+    return 100 - (INT32)load;
+}
+#endif // _WIN32
+
+///////////////////////////////////////////////////////////////////////////////////
+/// <summary>
 /// Helper method to compute the current CPU load on Linux.
 /// </summary>
 /// <returns>
-/// The total the current CPU load in percent.
+/// The total the current CPU load in percent, or -1 if it could not be determined.
 /// </returns>
 INT32 MgServerManager::ComputeLinuxCpuLoad()
 {
@@ -1295,49 +1377,34 @@
 
     #else
 
-        FILE* stat;
-        char line[80];
-        int user;
-        int nice;
-        int sys;
-        int idle;
-        int ouser;
-        int onice;
-        int osys;
-        int oidle;
-        double load;
+        CpuSample previous;
+        CpuSample current;
 
         // Prime initial values for computing the CPU load over the sample interval
-        stat = fopen( ( MgUtil::WideCharToMultiByte(MgUtil::LinuxFilesystemStat) ).c_str(), "r" );
-        if (NULL == stat)
+        if (!ReadLinuxCpuTimes(previous))
         {
-            MgStringCollection arguments;
-            arguments.Add(MgUtil::LinuxFilesystemStat);
-            throw new MgException(MgExceptionCodes::MgFileNotFoundException, L"MgServerManager.ComputeLinuxCpuLoad", __LINE__, __WFILE__, &arguments, L"", NULL);
+            return nCpuLoad;
         }
-        fgets(line, 80, stat);
-        sscanf(line, "%*3s %i %i %i %i", &ouser, &onice, &osys, &oidle);
-        fclose(stat);
 
-        // Wait one interval.
-        ACE_OS::sleep(1);
+        // Wait one interval. ACE_OS::sleep() does not restart an interrupted
+        // sleep, so retry: a sample taken over a near-zero interval has no
+        // counter movement to measure.
+        for (int attempts = 0; attempts < 3; ++attempts)
+        {
+            if (0 == ACE_OS::sleep(1))
+            {
+                break;
+            }
+        }
 
         // Get the current data values
-        stat = fopen( ( MgUtil::WideCharToMultiByte(MgUtil::LinuxFilesystemStat) ).c_str(), "r");
-        if(NULL == stat)
+        if (!ReadLinuxCpuTimes(current))
         {
-            MgStringCollection arguments;
-            arguments.Add(MgUtil::LinuxFilesystemStat);
-            throw new MgException(MgExceptionCodes::MgFileNotFoundException, L"MgServerManager.ComputeLinuxCpuLoad", __LINE__, __WFILE__, &arguments, L"", NULL);
+            return nCpuLoad;
         }
-        fgets(line, 80, stat);
-        sscanf(line, "%*3s %i %i %i %i", &user, &nice, &sys, &idle);
-        fclose(stat);
 
-        load = (double)(idle-oidle) / (double)( (user-ouser) + (nice-onice) + (sys-osys) + (idle-oidle) ) * 100;
+        nCpuLoad = ComputeCpuUtilization(previous, current);
 
-        nCpuLoad = 100 - (INT32)load;
-
     #endif  // _WIN32
 
     return nCpuLoad;

Modified: branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.h
===================================================================
--- branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.h	2026-10-19 16:06:14 UTC (rev 10219)
+++ branches/4.0/MgDev/Server/src/Common/Manager/ServerManager.h	2026-10-21 08:23:18 UTC (rev 10220)
@@ -141,6 +141,21 @@
     void StartWorkerThread(void (*function)());
     void StopWorkerThreads();
 
+#ifndef _WIN32
+    /// Aggregate CPU counters from the "cpu" line of /proc/stat, in USER_HZ jiffies
+    struct CpuSample
+    {
+        UINT64 user;
+        UINT64 nice;
+        UINT64 sys;
+        UINT64 idle;
+    };
+
+    /// Computes the CPU utilization in percent between two /proc/stat samples.
+    /// Returns -1 if the samples cannot be compared.
+    static INT32 ComputeCpuUtilization(const CpuSample& previous, const CpuSample& current);
+#endif
+
 private:
 
     // Constructor

Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestServerAdminService.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestServerAdminService.cpp	2026-10-19 16:06:14 UTC (rev 10219)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestServerAdminService.cpp	2026-10-21 08:23:18 UTC (rev 10220)
@@ -504,6 +504,16 @@
 
         Ptr<MgPropertyCollection> pPropertyCollection = MgServerManager::GetInstance()->GetInformationProperties();
         REQUIRE(0 < pPropertyCollection->GetCount());
+
+        // The CPU utilization is derived from /proc/stat, so it is a percentage
+        // or -1 when no sample could be taken (see MgServerManager::ComputeCpuUtilization)
+        Ptr<MgProperty> pCpuProperty = pPropertyCollection->GetItem(MgServerInformationProperties::CpuUtilization);
+        REQUIRE(pCpuProperty != nullptr);
+        REQUIRE(MgPropertyType::Int32 == pCpuProperty->GetPropertyType());
+
+        INT32 nCpuUtilization = ((MgInt32Property*)pCpuProperty.p)->GetValue();
+        REQUIRE(-1 <= nCpuUtilization);
+        REQUIRE(100 >= nCpuUtilization);
     }
     catch(MgException* e)
     {

Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestServerManager.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestServerManager.cpp	2026-10-19 16:06:14 UTC (rev 10219)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestServerManager.cpp	2026-10-21 08:23:18 UTC (rev 10220)
@@ -174,4 +174,81 @@
     {
         throw;
     }
-}
\ No newline at end of file
+}
+#ifndef _WIN32
+///----------------------------------------------------------------------------
+/// Test Case Description:
+///
+/// This test case checks the CPU utilization derived from two /proc/stat
+/// samples. This is what the performance log's CPU column is built from, and
+/// the values it used to produce after a long uptime (100, then -2147483548)
+/// are the reason the samples are read as 64-bit counters and the ratio is
+/// guarded rather than divided blindly.
+///
+/// Linux only, because Windows takes its CPU load from a PDH counter that
+/// already returns a percentage and never builds cumulative counter samples.
+///----------------------------------------------------------------------------
+TEST_CASE("ComputeCpuUtilization", "[ServerManager]")
+{
+    typedef MgServerManager::CpuSample Sample;
+
+    // One second of a 4-core host, counters well short of any 32-bit limit
+    const Sample before = { 1000, 10, 500, 90000 };
+
+    SECTION("utilization between two samples")
+    {
+        // +300 user, +100 sys, +100 idle: 20% of the interval was idle
+        const Sample after = { 1300, 10, 600, 90100 };
+
+        REQUIRE(80 == MgServerManager::ComputeCpuUtilization(before, after));
+    }
+
+    SECTION("an interval spent entirely busy is 100")
+    {
+        const Sample after = { 1800, 10, 700, 90000 };
+
+        REQUIRE(100 == MgServerManager::ComputeCpuUtilization(before, after));
+    }
+
+    SECTION("an interval spent entirely idle is 0")
+    {
+        const Sample after = { 1000, 10, 500, 90250 };
+
+        REQUIRE(0 == MgServerManager::ComputeCpuUtilization(before, after));
+    }
+
+    SECTION("an interval with no counter movement has no utilization")
+    {
+        // The 32-bit read of these counters clamped at INT_MAX, so an idle
+        // system stopped moving and 0/0 ended up logged as -2147483548.
+        const Sample after = before;
+
+        REQUIRE(-1 == MgServerManager::ComputeCpuUtilization(before, after));
+    }
+
+    SECTION("a counter that moved backwards has no utilization")
+    {
+        // A CPU hot-unplugged between the two samples takes its time with it
+        const Sample lessIdle = { 1000, 10, 500, 89900 };
+        const Sample lessUser = { 900, 10, 500, 90000 };
+
+        REQUIRE(-1 == MgServerManager::ComputeCpuUtilization(before, lessIdle));
+        REQUIRE(-1 == MgServerManager::ComputeCpuUtilization(before, lessUser));
+    }
+
+    SECTION("counters past 2^31 and 2^32 keep their deltas")
+    {
+        // Months of uptime on a many-core host: idle crosses 2^31 inside the
+        // interval, which is the point where a 32-bit read stops advancing.
+        const Sample nearIntMax = { 1000000000, 10, 900000000, 2147483600 };
+        const Sample pastIntMax = { 1000000300, 10, 900000100, 2147483700 };
+        REQUIRE(80 == MgServerManager::ComputeCpuUtilization(nearIntMax, pastIntMax));
+
+        // Every counter here is larger than a 32-bit value can hold: +900 user,
+        // +300 sys, +1800 idle
+        const Sample past4Gb = { 4294967296ULL, 0, 1000000000ULL, 9999999999ULL };
+        const Sample past4GbAfter = { 4294967296ULL + 900, 0, 1000000000ULL + 300, 9999999999ULL + 1800 };
+        REQUIRE(40 == MgServerManager::ComputeCpuUtilization(past4Gb, past4GbAfter));
+    }
+}
+#endif // _WIN32



More information about the mapguide-commits mailing list