diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -317,6 +317,8 @@ 'PhutilSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilSyntaxHighlighter.php', 'PhutilSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilSyntaxHighlighterEngine.php', 'PhutilSyntaxHighlighterException' => 'markup/syntax/highlighter/PhutilSyntaxHighlighterException.php', + 'PhutilSystem' => 'utils/PhutilSystem.php', + 'PhutilSystemTestCase' => 'utils/__tests__/PhutilSystemTestCase.php', 'PhutilTestCase' => 'infrastructure/testing/PhutilTestCase.php', 'PhutilTestPhobject' => 'object/__tests__/PhutilTestPhobject.php', 'PhutilTortureTestDaemon' => 'daemon/torture/PhutilTortureTestDaemon.php', @@ -715,6 +717,8 @@ 'PhutilSimpleOptionsTestCase' => 'PhutilTestCase', 'PhutilSocketChannel' => 'PhutilChannel', 'PhutilSyntaxHighlighterException' => 'Exception', + 'PhutilSystem' => 'Phobject', + 'PhutilSystemTestCase' => 'PhutilTestCase', 'PhutilTestCase' => 'ArcanistPhutilTestCase', 'PhutilTestPhobject' => 'Phobject', 'PhutilTortureTestDaemon' => 'PhutilDaemon', diff --git a/src/utils/PhutilSystem.php b/src/utils/PhutilSystem.php new file mode 100644 --- /dev/null +++ b/src/utils/PhutilSystem.php @@ -0,0 +1,158 @@ +<?php + +/** + * Interact with the operating system. + * + * @task memory Interacting with System Memory + */ +final class PhutilSystem extends Phobject { + + + /** + * Get information about total and free memory on the system. + * + * Because "free memory" is a murky concept, the interpretation of the values + * returned from this method will vary from system to system and the numbers + * themselves may be only roughly accurate. + * + * @return map<string, wild> Dictionary of memory information. + * @task memory + */ + public static function getSystemMemoryInformation() { + $meminfo_path = '/proc/meminfo'; + if (Filesystem::pathExists($meminfo_path)) { + $meminfo_data = Filesystem::readFile($meminfo_path); + return self::parseMemInfo($meminfo_data); + } else if (Filesystem::binaryExists('vm_stat')) { + list($vm_stat_stdout) = execx('vm_stat'); + return self::parseVMStat($vm_stat_stdout); + } else { + throw new Exception( + pht( + 'Unable to access /proc/meminfo or `vm_stat` on this system to '. + 'get system memory information.')); + } + } + + + /** + * Parse the output of `/proc/meminfo`. + * + * See @{method:getSystemMemoryInformation}. This method is used to get memory + * information on Linux. + * + * @param string Raw `/proc/meminfo`. + * @return map<string, wild> Parsed memory information. + * @task memory + */ + public static function parseMemInfo($data) { + $data = phutil_split_lines($data); + + $map = array(); + foreach ($data as $line) { + list($key, $value) = explode(':', $line, 2); + $key = trim($key); + $value = trim($value); + + $matches = null; + if (preg_match('/^(\d+) kB\z/', $value, $matches)) { + $value = (int)$matches[1] * 1024; + } + + $map[$key] = $value; + } + + $expect = array( + 'MemTotal', + 'MemFree', + 'Buffers', + 'Cached', + ); + foreach ($expect as $key) { + if (!array_key_exists($key, $map)) { + throw new Exception( + pht( + 'Expected to find "%s" in "/proc/meminfo" output, but did not.', + $key)); + } + } + + $total = $map['MemTotal']; + $free = $map['MemFree'] + $map['Buffers'] + $map['Cached']; + + return array( + 'total' => $total, + 'free' => $free, + ); + } + + + /** + * Parse the output of `vm_stat`. + * + * See @{method:getSystemMemoryInformation}. This method is used to get memory + * information on Mac OS X. + * + * @param string Raw `vm_stat` output. + * @return map<string, wild> Parsed memory information. + * @task memory + */ + public static function parseVMStat($data) { + $data = phutil_split_lines($data); + + $page_size = null; + $map = array(); + + foreach ($data as $line) { + list($key, $value) = explode(':', $line, 2); + $key = trim($key); + $value = trim($value); + + $matches = null; + if (preg_match('/page size of (\d+) bytes/', $value, $matches)) { + $page_size = (int)$matches[1]; + continue; + } + + $value = trim($value, '.'); + $map[$key] = $value; + } + + if (!$page_size) { + throw new Exception( + pht( + 'Expected to find "page size" in `vm_stat` output, but did not.')); + } + + $expect = array( + 'Pages free', + 'Pages active', + 'Pages inactive', + 'Pages wired down', + ); + foreach ($expect as $key) { + if (!array_key_exists($key, $map)) { + throw new Exception( + pht( + 'Expected to find "%s" in `vm_stat` output, but did not.', + $key)); + } + } + + // NOTE: This calculation probably isn't quite right. In particular, + // the numbers don't exactly add up, and "Pages inactive" includes a + // bunch of disk cache. So these numbers aren't totally reliable and they + // aren't directly comparable to the /proc/meminfo numbers. + + $free = $map['Pages free']; + $active = $map['Pages active']; + $inactive = $map['Pages inactive']; + $wired = $map['Pages wired down']; + + return array( + 'total' => ($free + $active + $inactive + $wired) * $page_size, + 'free' => ($free) * $page_size, + ); + } + +} diff --git a/src/utils/__tests__/PhutilSystemTestCase.php b/src/utils/__tests__/PhutilSystemTestCase.php new file mode 100644 --- /dev/null +++ b/src/utils/__tests__/PhutilSystemTestCase.php @@ -0,0 +1,43 @@ +<?php + +final class PhutilSystemTestCase extends PhutilTestCase { + + public function testParseVMStat() { + $tests = array( + 'vmstat.yosemite.txt' => array( + 'total' => 16503578624, + 'free' => 1732366336, + ), + ); + + $dir = dirname(__FILE__).'/memory'; + foreach ($tests as $input => $expect) { + $raw = Filesystem::readFile($dir.'/'.$input); + $actual = PhutilSystem::parseVMStat($raw); + $this->assertEqual( + $expect, + $actual, + pht('Parse of "%s".', $input)); + } + } + + public function testParseMeminfo() { + $tests = array( + 'meminfo.ubuntu14.txt' => array( + 'total' => 7843336192, + 'free' => 3758297088, + ), + ); + + $dir = dirname(__FILE__).'/memory'; + foreach ($tests as $input => $expect) { + $raw = Filesystem::readFile($dir.'/'.$input); + $actual = PhutilSystem::parseMemInfo($raw); + $this->assertEqual( + $expect, + $actual, + pht('Parse of "%s".', $input)); + } + } + +} diff --git a/src/utils/__tests__/memory/meminfo.ubuntu14.txt b/src/utils/__tests__/memory/meminfo.ubuntu14.txt new file mode 100644 --- /dev/null +++ b/src/utils/__tests__/memory/meminfo.ubuntu14.txt @@ -0,0 +1,42 @@ +MemTotal: 7659508 kB +MemFree: 246684 kB +Buffers: 126580 kB +Cached: 3296948 kB +SwapCached: 0 kB +Active: 4916076 kB +Inactive: 1732880 kB +Active(anon): 3225504 kB +Inactive(anon): 20576 kB +Active(file): 1690572 kB +Inactive(file): 1712304 kB +Unevictable: 0 kB +Mlocked: 0 kB +SwapTotal: 0 kB +SwapFree: 0 kB +Dirty: 32 kB +Writeback: 0 kB +AnonPages: 3225428 kB +Mapped: 37500 kB +Shmem: 20652 kB +Slab: 633908 kB +SReclaimable: 467472 kB +SUnreclaim: 166436 kB +KernelStack: 2416 kB +PageTables: 64744 kB +NFS_Unstable: 0 kB +Bounce: 0 kB +WritebackTmp: 0 kB +CommitLimit: 3829752 kB +Committed_AS: 4935116 kB +VmallocTotal: 34359738367 kB +VmallocUsed: 17244 kB +VmallocChunk: 34359712740 kB +HardwareCorrupted: 0 kB +AnonHugePages: 954368 kB +HugePages_Total: 0 +HugePages_Free: 0 +HugePages_Rsvd: 0 +HugePages_Surp: 0 +Hugepagesize: 2048 kB +DirectMap4k: 45056 kB +DirectMap2M: 7950336 kB diff --git a/src/utils/__tests__/memory/vmstat.yosemite.txt b/src/utils/__tests__/memory/vmstat.yosemite.txt new file mode 100644 --- /dev/null +++ b/src/utils/__tests__/memory/vmstat.yosemite.txt @@ -0,0 +1,23 @@ +Mach Virtual Memory Statistics: (page size of 4096 bytes) +Pages free: 422941. +Pages active: 2348641. +Pages inactive: 830440. +Pages speculative: 110635. +Pages throttled: 0. +Pages wired down: 427172. +Pages purgeable: 33368. +"Translation faults": 931955891. +Pages copy-on-write: 59498342. +Pages zero filled: 411628732. +Pages reactivated: 175636. +Pages purged: 569552. +File-backed pages: 926777. +Anonymous pages: 2362939. +Pages stored in compressor: 125673. +Pages occupied by compressor: 51938. +Decompressions: 32945. +Compressions: 197789. +Pageins: 13750115. +Pageouts: 39562. +Swapins: 0. +Swapouts: 2290.