Files
XC_VM/tests/Unit/SystemInfoGuardsTest.php
T
Claude f8f3ca3a82 fix(watchdog): correct bandwidth totals, rps, producers and fatals
SystemInfo::getStats() feeds the watchdog heartbeat (servers.watchdog_data),
servers_stats and the LB /api stats action, and had four defects:

- bytes_sent_total/bytes_received_total were assigned per interface, so
  only the last selected interface was kept, and the received total read
  tx_bytes. aggregateNetwork() now sums both totals over the same
  interfaces as the per-second rates and reads rx_bytes for received.
  network_speed keeps its first-positive rule and the interface selection
  is unchanged, so routing capacity is not affected.
- total_running_streams was `ps ax | grep -c ffmpeg`, which counted
  ffprobe and shell wrappers and missed the fanout daemon's native
  remuxer. ProcessManager::countStreamProducers() now reads /proc and
  counts processes whose program is ffmpeg or `xc_fanout remux`.
- The memory percentage divided by total_mem, which getMemory() reports
  as 0 without MemAvailable (DivisionByZeroError), and nvidia-smi XML
  with a single GPU process decodes process_info as one associative
  array (TypeError). Either aborted the watchdog pass, so the node wrote
  no heartbeat. memUsedPercent() and gpuProcesses() guard both.

The watchdog runs one pass per process and re-execs itself, so the
previous nginx request count held in a variable was always null and
requests_per_second was 0 on every node. CounterRateSampler keeps the
previous reading in a tmp state file (atomic replace, 120 s max age, 0
after a counter reset).

The watchdog_data key set and order are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQHeypQpfAXTdCysmi7b6a
2026-09-25 00:10:15 +00:00

72 lines
3.0 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
use XcVm\Core\Util\SystemInfo;
/**
* Two SystemInfo::getStats() fatals that aborted the watchdog pass, so the
* node wrote no heartbeat and showed offline:
*
* - the memory percentage divided by total_mem, which getMemory() reports as
* 0 when /proc/meminfo has no MemAvailable or cannot be read
* (DivisionByZeroError);
* - nvidia-smi XML with exactly one GPU process decodes process_info as one
* associative array, so iterating it yielded strings and `$rProcess['pid']`
* threw a TypeError.
*/
final class SystemInfoGuardsTest extends TestCase {
public function testMemoryPercentWithNoTotalIsZero(): void {
$this->assertSame(0.0, SystemInfo::memUsedPercent(0, 0));
$this->assertSame(0.0, SystemInfo::memUsedPercent(512, 0));
}
public function testMemoryPercentIsRoundedToTwoPlaces(): void {
$this->assertSame(25.0, SystemInfo::memUsedPercent(1024, 4096));
$this->assertSame(33.33, SystemInfo::memUsedPercent(1, 3));
}
public function testASingleGpuProcessIsWrappedInAList(): void {
$rInstance = ['processes' => ['process_info' => ['pid' => '4242', 'type' => 'C', 'process_name' => 'ffmpeg', 'used_memory' => '312 MiB']]];
$this->assertSame([['pid' => 4242, 'memory' => '312 MiB']], SystemInfo::gpuProcesses($rInstance));
}
public function testAListOfGpuProcessesIsMappedInOrder(): void {
$rInstance = ['processes' => ['process_info' => [
['pid' => '11', 'used_memory' => '100 MiB'],
['pid' => '12', 'used_memory' => '200 MiB'],
]]];
$this->assertSame(
[['pid' => 11, 'memory' => '100 MiB'], ['pid' => 12, 'memory' => '200 MiB']],
SystemInfo::gpuProcesses($rInstance)
);
}
public function testNoGpuProcessesIsAnEmptyList(): void {
$this->assertSame([], SystemInfo::gpuProcesses([]));
// An empty <processes/> element decodes to an empty array.
$this->assertSame([], SystemInfo::gpuProcesses(['processes' => []]));
$this->assertSame([], SystemInfo::gpuProcesses(['processes' => 'N/A']));
$this->assertSame([], SystemInfo::gpuProcesses(['processes' => ['process_info' => []]]));
}
public function testGpuProcessesDecodedFromNvidiaSmiXml(): void {
$rXML = '<?xml version="1.0" ?><nvidia_smi_log><gpu id="00000000:01:00.0"><processes>'
. '<process_info><pid>777</pid><type>C</type><process_name>ffmpeg</process_name><used_memory>150 MiB</used_memory></process_info>'
. '</processes></gpu></nvidia_smi_log>';
$rGPU = json_decode(json_encode(simplexml_load_string($rXML)), true)['gpu'];
$this->assertSame([['pid' => 777, 'memory' => '150 MiB']], SystemInfo::gpuProcesses($rGPU));
}
public function testGetStatsUsesTheGuardedHelpers(): void {
$rSource = (string) file_get_contents(MAIN_HOME . 'Core/Util/SystemInfo.php');
$this->assertStringContainsString("self::memUsedPercent(\$rJSON['total_mem_used'], \$rJSON['total_mem'])", $rSource);
$this->assertStringContainsString("\$rArray['processes'] = self::gpuProcesses(\$rInstance);", $rSource);
$this->assertStringNotContainsString("\$rInstance['processes']['process_info'] as", $rSource);
}
}