From eb9301d0a174d7bf3261f583994e2e7d2fed6d40 Mon Sep 17 00:00:00 2001 From: riki137 Date: Mon, 9 Mar 2026 12:41:57 +0100 Subject: [PATCH 1/2] refactor: improve memory usage reporting and add SystemMemoryReader class --- CHANGELOG.md | 5 +- .../Output/SystemMemoryReader.php | 64 +++++++++ src/Orchestrator/Output/TableOutput.php | 31 +---- .../Output/TableOutputFactory.php | 2 +- tests/Unit/SystemMemoryReaderTest.php | 122 ++++++++++++++++++ 5 files changed, 197 insertions(+), 27 deletions(-) create mode 100644 src/Orchestrator/Output/SystemMemoryReader.php create mode 100644 tests/Unit/SystemMemoryReaderTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a88180..31f5241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Improved +- extracted system memory probing from TableOutput into a dedicated SystemMemoryReader - +### Fixed +- table output memory reporting on BusyBox environments without `ps -p` ## [1.1.0] - 2026-02-12 ### Added diff --git a/src/Orchestrator/Output/SystemMemoryReader.php b/src/Orchestrator/Output/SystemMemoryReader.php new file mode 100644 index 0000000..b939a1a --- /dev/null +++ b/src/Orchestrator/Output/SystemMemoryReader.php @@ -0,0 +1,64 @@ +procMemoryUsage($pid) ?? $this->psMemoryUsage($pid); + if ($rss !== null) { + return $rss; + } + } + + return memory_get_usage(true); + } + + public function getFreeMemory(): ?int + { + if (!is_readable('/proc/meminfo')) { + return null; + } + + $data = file_get_contents('/proc/meminfo'); + if (preg_match('/MemAvailable:\s+(\d+)/', (string)$data, $m)) { + return (int)$m[1] * 1024; + } + + return null; + } + + private function procMemoryUsage(int $pid): ?int + { + $statusFile = '/proc/' . $pid . '/status'; + if (!is_readable($statusFile)) { + return null; + } + + $data = file_get_contents($statusFile); + if (!is_string($data) || $data === '') { + return null; + } + + if (preg_match('/^VmRSS:\s+(\d+)\s+kB$/mi', $data, $m)) { + return (int)$m[1] * 1024; + } + + return null; + } + + private function psMemoryUsage(int $pid): ?int + { + $out = @shell_exec('ps -o rss= -p ' . $pid); + if (preg_match('/^\s*(\d+)\s*$/', (string)$out, $m)) { + return (int)$m[1] * 1024; + } + + return null; + } +} diff --git a/src/Orchestrator/Output/TableOutput.php b/src/Orchestrator/Output/TableOutput.php index 93e9239..8278dc7 100644 --- a/src/Orchestrator/Output/TableOutput.php +++ b/src/Orchestrator/Output/TableOutput.php @@ -24,6 +24,8 @@ final class TableOutput implements ProgressOutput private readonly TableRenderer $renderer; + private readonly SystemMemoryReader $memoryReader; + /** @var array */ private array $states = []; @@ -35,6 +37,7 @@ public function __construct( TaskList $taskList, private readonly bool $interactive, private readonly int $lowMemoryWarning, + ?SystemMemoryReader $memoryReader = null, ) { if ($interactive && $output instanceof ConsoleOutputInterface) { $this->section = $output->section(); @@ -42,6 +45,7 @@ public function __construct( $this->section = $output; } $this->renderer = new TableRenderer($taskList); + $this->memoryReader = $memoryReader ?? new SystemMemoryReader(); } public function onTaskStarted(TaskState $state): void @@ -90,7 +94,7 @@ private function buildSectionBuffer(): array $this->attachMemoryWarning($sectionBuffer); $sectionBuffer[] = $this->renderer->getSummaryRow( $partiallyDone, - self::memoryUsage(), + $this->memoryReader->getCurrentProcessUsage(), $workersMem, ); @@ -125,29 +129,6 @@ public function render(): void } } - private static function memoryUsage(): int - { - $pid = getmypid(); - $out = @shell_exec('ps -o rss= -p ' . $pid); - if (is_string($out) && trim($out) !== '') { - return (int)trim($out) * 1024; - } - - return memory_get_usage(true); - } - - private static function freeMemory(): ?int - { - if (is_readable('/proc/meminfo')) { - $data = file_get_contents('/proc/meminfo'); - if (preg_match('/MemAvailable:\s+(\d+)/', (string)$data, $m)) { - return (int)$m[1] * 1024; - } - } - - return null; - } - public function __destruct() { if ($this->interactive) { @@ -167,7 +148,7 @@ private function attachMemoryWarning(array &$buffer): void if ($this->lowMemoryWarning < 1) { return; } - $freeMem = self::freeMemory(); + $freeMem = $this->memoryReader->getFreeMemory(); if ($freeMem !== null && $freeMem < ($this->lowMemoryWarning * self::MB)) { $buffer[] = $this->renderer->getRowLabel('LOW MEMORY', TaskStatus::SKIP) . diff --git a/src/Orchestrator/Output/TableOutputFactory.php b/src/Orchestrator/Output/TableOutputFactory.php index 80e5dd3..880f4a8 100644 --- a/src/Orchestrator/Output/TableOutputFactory.php +++ b/src/Orchestrator/Output/TableOutputFactory.php @@ -43,7 +43,7 @@ public function create(TaskList $taskList, OutputInterface $output, IpcHandlerRe throw new InvalidArgumentException('Low memory warning must be a positive integer.'); } - $table = new TableOutput($output, $taskList, $interactive, $lowMemoryWarning); + $table = new TableOutput($output, $taskList, $interactive, $lowMemoryWarning, new SystemMemoryReader()); $registry->onMessage(function (Message $message, TaskState $state) use ($table) { if ($message instanceof LogMessage) { $table->log($state, $message->message); diff --git a/tests/Unit/SystemMemoryReaderTest.php b/tests/Unit/SystemMemoryReaderTest.php new file mode 100644 index 0000000..23087eb --- /dev/null +++ b/tests/Unit/SystemMemoryReaderTest.php @@ -0,0 +1,122 @@ + */ + public static array $readableFiles = []; + + /** @var array */ + public static array $fileContents = []; + + /** @var string[] */ + public static array $shellCommands = []; + + public static string $shellOutput = ''; + + public static int $memoryFallback = 0; + + protected function setUp(): void + { + $this->resetDoubles(); + } + + protected function tearDown(): void + { + $this->resetDoubles(); + } + + public function testCurrentProcessUsagePrefersProcStatusOverPs(): void + { + $statusFile = '/proc/4321/status'; + self::$readableFiles[$statusFile] = true; + self::$fileContents[$statusFile] = "Name:\tphp\nVmRSS:\t12345 kB\n"; + self::$shellOutput = "99999\n"; + self::$memoryFallback = 777; + + $reader = new SystemMemoryReader(); + + $this->assertSame(12345 * 1024, $reader->getCurrentProcessUsage()); + $this->assertSame([], self::$shellCommands); + } + + public function testCurrentProcessUsageFallsBackToPsWhenProcIsUnavailable(): void + { + self::$shellOutput = "23456\n"; + self::$memoryFallback = 777; + + $reader = new SystemMemoryReader(); + + $this->assertSame(23456 * 1024, $reader->getCurrentProcessUsage()); + $this->assertSame(['ps -o rss= -p 4321'], self::$shellCommands); + } + + public function testCurrentProcessUsageFallsBackToPhpAllocatorWhenSystemLookupsFail(): void + { + self::$shellOutput = "ps: unrecognized option: p\n"; + self::$memoryFallback = 345678; + + $reader = new SystemMemoryReader(); + + $this->assertSame(345678, $reader->getCurrentProcessUsage()); + $this->assertSame(['ps -o rss= -p 4321'], self::$shellCommands); + } + + public function testFreeMemoryReadsMemAvailableFromProcMeminfo(): void + { + self::$readableFiles['/proc/meminfo'] = true; + self::$fileContents['/proc/meminfo'] = "MemTotal: 1 kB\nMemAvailable: 2048 kB\n"; + + $reader = new SystemMemoryReader(); + + $this->assertSame(2048 * 1024, $reader->getFreeMemory()); + } + + private function resetDoubles(): void + { + self::$pid = 4321; + self::$readableFiles = []; + self::$fileContents = []; + self::$shellCommands = []; + self::$shellOutput = ''; + self::$memoryFallback = 0; + } +} + +} From e2491c8f7d33fd0c64e2e5af64c2cd91b65c62cd Mon Sep 17 00:00:00 2001 From: riki137 Date: Mon, 9 Mar 2026 12:48:52 +0100 Subject: [PATCH 2/2] fix test coverage --- tests/Unit/SystemMemoryReaderTest.php | 32 +++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tests/Unit/SystemMemoryReaderTest.php b/tests/Unit/SystemMemoryReaderTest.php index 23087eb..2756007 100644 --- a/tests/Unit/SystemMemoryReaderTest.php +++ b/tests/Unit/SystemMemoryReaderTest.php @@ -3,7 +3,7 @@ declare(strict_types=1); namespace Multitron\Orchestrator\Output { - function getmypid(): int + function getmypid(): int|false { return \Multitron\Tests\Unit\SystemMemoryReaderTest::$pid; } @@ -37,7 +37,7 @@ function memory_get_usage(bool $realUsage = false): int final class SystemMemoryReaderTest extends TestCase { - public static int $pid = 4321; + public static int|false $pid = 4321; /** @var array */ public static array $readableFiles = []; @@ -98,6 +98,17 @@ public function testCurrentProcessUsageFallsBackToPhpAllocatorWhenSystemLookupsF $this->assertSame(['ps -o rss= -p 4321'], self::$shellCommands); } + public function testCurrentProcessUsageFallsBackToPhpAllocatorWhenPidLookupFails(): void + { + self::$pid = false; + self::$memoryFallback = 456789; + + $reader = new SystemMemoryReader(); + + $this->assertSame(456789, $reader->getCurrentProcessUsage()); + $this->assertSame([], self::$shellCommands); + } + public function testFreeMemoryReadsMemAvailableFromProcMeminfo(): void { self::$readableFiles['/proc/meminfo'] = true; @@ -108,6 +119,23 @@ public function testFreeMemoryReadsMemAvailableFromProcMeminfo(): void $this->assertSame(2048 * 1024, $reader->getFreeMemory()); } + public function testFreeMemoryReturnsNullWhenProcMeminfoIsUnavailable(): void + { + $reader = new SystemMemoryReader(); + + $this->assertNull($reader->getFreeMemory()); + } + + public function testFreeMemoryReturnsNullWhenMemAvailableIsMissing(): void + { + self::$readableFiles['/proc/meminfo'] = true; + self::$fileContents['/proc/meminfo'] = "MemTotal: 1 kB\nMemFree: 512 kB\n"; + + $reader = new SystemMemoryReader(); + + $this->assertNull($reader->getFreeMemory()); + } + private function resetDoubles(): void { self::$pid = 4321;