[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