[benchmarks] Memory handling and cachegrind parsing

From: Date: Tue, 09 Jun 2009 14:58:49 +0000
Subject: [benchmarks] Memory handling and cachegrind parsing
Groups: php.qa 
Request: Send a blank email to php-qa+get-65078@lists.php.net to get a copy of this message
Hi I've added the diffs to bench.php here as well as the cachegrindparser.php and smapsparser.php. None of these are completely implemented, but I'm on my way and think it's good to hear some feedback here. The cachegrind usage has no output at all currently. I chose to implement the smaps memory handling because I couldn't find anything that was suiting better. If there would have been a fast tool that worked on windows as well as linux, it would have been better, but since google used this in their "unladen shadow" project, I thought it was a good idea to do it here as well. It works by sampling the memory data during the runs and place the info in an array, which then takes the maximum memory usage and prints it along with the files that consumed the most memory during that time. I used a function from the php-compiler to process a php command instead of just exec() since that was needed. There is one "problem" about the pid for the test processes. I have to use "pid + 1" to get the actual pid, since the "pid" is just the shell execution and "pid + 1" is the real php execution. Could this be a problem? Thank you and regards Alexander

Index: bench.php =================================================================== RCS file: /repository/php-benchmarks/benchcli/bench.php,v retrieving revision 1.2 diff -u -p -r1.2 bench.php --- bench.php 28 May 2009 13:40:59 -0000 1.2 +++ bench.php 9 Jun 2009 14:45:19 -0000 @@ -79,20 +79,40 @@ class Benchmark */ var $test_files; + + /** + * Whether cachegrind should be used or not + * @var boolean + */ + var $cachegrind; + + /** + * Class for parsing and sorting Smaps-files + * @var object + */ + var $smapsparser; + + /** + * Switch for showing memory usage or not + * @var boolean + */ + var $mem_usage; + /** * Constructor for the benchmark class. Uses the PEAR package * Console_getargs for command line handling. * * @return void */ - function Benchmark() + function Benchmark() { include_once 'misc/getargs.php'; include_once 'misc/timer.php'; - + include_once 'misc/smapsparser.php'; + include_once 'misc/cachegrindparser.php'; $this->setConfig(); - $args =& Console_Getargs::factory($this->config); - + $args = &Console_Getargs::factory($this->config); + $this->smapsparser = new Smapsparser(); if (PEAR::isError($args) || $args->getValue('help')) { $this->printHelp($args); exit; @@ -122,18 +142,20 @@ class Benchmark if ($this->log_file == "") { $this->log_file = false; } - $this->quite = $args->getValue('quite'); + $this->quite = $args->getValue('quite'); + $this->mem_usage = $args->getValue('memory-usage'); + $this->cachegrind = $args->getValue('cachegrind'); $this->setTestFiles(); } /** * Prints message to the screen - * + * * @param string $msg The message to print * @param bool $print Whether to print or not print - * + * * @return void */ - function console($msg, $print=true) + function console($msg, $print=true) { if (!$this->quite) { if ($print) { @@ -150,10 +172,10 @@ class Benchmark * * @return void */ - function setConfig() + function setConfig() { $this->config = array( - 'memory-limit' => array('short' => 'm', + 'memory-limit' => array('short' => 'ml', 'min' => 0, 'max' => 1, 'default' => 128, @@ -174,6 +196,11 @@ class Benchmark 'quite' => array('short' => 'q', 'max' => 0, 'desc' => 'Don\'t produce any output'), + 'memory-usage' => array('short' => 'mu', + 'max' => 0, + 'desc' => 'Show memory statistics'), + 'cachegrind' => array('max' => 0, + 'desc' => 'Enable the valgrind tool that a cache simulation of the test'), 'log' => array('min' => 0, 'max' => 1, 'default' => 'benchlog.txt', @@ -183,12 +210,12 @@ class Benchmark /** * Prints the help message for the benchmark - * + * * @param array $args The arguments to be listed - * + * * @return void */ - function printHelp($args) + function printHelp($args) { $header = "Php Benchmark Example\n". 'Usage: '.basename($_SERVER['SCRIPT_NAME'])." [options]\n\n"; @@ -231,24 +258,42 @@ class Benchmark /** * Runs the benchmark - * + * * @return void */ - function run() + function run() { - $timer = new Timer(); - $totaltime = 0; - $datetime = date("Y-m-d H:i:s", time()); - + $timer = new Timer(); + $totaltime = 0; + $datetime = date("Y-m-d H:i:s", time()); + $startstring = ""; $this->console("--------------- Bench.php ".$datetime."--------------\n"); foreach ($this->test_files as $test) { - $cmd = "{$this->php} -d memory_limit={$this->memory_limit}M ".$test['filename']; + $output = array(); + if ($this->cachegrind) { + $startstring = "valgrind --tool=cachegrind --branch-sim=yes"; + } + $cmd = "$startstring {$this->php} -d memory_limit={$this->memory_limit}M ".$test['filename']; $this->console("$cmd\n", $this->debug); $timer->start(); - $debug = `$cmd`; + list($out, $err, $exit) = $this->completeExec($cmd, null, 0); + if ($this->mem_usage) { + if (!$this->quite) { + $this->smapsparser->printMaxUsage(10); + } + if ($this->log_file) { + $this->smapsparser->printMaxUsage(10, $this->log_file); + } + $this->smapsparser->clear(); + } + if ($this->cachegrind) { + $cacheparser = new Cachegrindparser(); + $result = $cacheparser->parse($err); + print_r($result); + } $timer->stop(); - $this->console($debug, $this->debug); + $this->console($out, $this->debug); $this->console("Results from ".$test['name'].": ".$timer->elapsed."\n"); $totaltime += $timer->elapsed; } @@ -258,6 +303,115 @@ class Benchmark $datetime = date("Y-m-d H:i:s", time()); $this->console("-------------- END ".$datetime."---------------------\n"); } + /** + * Executes a program in proper way. The function is borrowed + * the php compiler project, http://www.phpcompiler.org. + * + * @param string $command The command to be executed + * @param string $stdin stdin + * @param int $timeout Seconds until it timeouts + * + * @return void + */ + function completeExec($command, $stdin = null, $timeout = 20) + { + $descriptorspec = array(0 => array("pipe", "r"), + 1 => array("pipe", "w"), + 2 => array("pipe", "w")); + $pipes = array(); + $handle = proc_open($command, $descriptorspec, &$pipes, getcwd()); + + // read stdin into the process + if ($stdin !== null) { + fwrite($pipes[0], $stdin); + } + fclose($pipes[0]); + unset($pipes[0]); + + // set non blocking to avoid infinite loops on stuck programs + stream_set_blocking($pipes[1], 0); + stream_set_blocking($pipes[2], 0); + + $out = ""; + $err = ""; + + $start_time = time(); + do { + $status = proc_get_status($handle); + + // It seems that with a large amount fo output, the process + // won't finish unless the buffers are periodically cleared. + // (This doesn't seem to be the case is async_test. I don't + // know why). + $new_out = stream_get_contents($pipes[1]); + $new_err = stream_get_contents($pipes[2]); + $out .= $new_out; + $err .= $new_err; + if ($this->mem_usage) { + if ($data = $this->smapsparser->readSmapsData($status["pid"]+1)) { + $this->smapsparser->parseSmapsData($data); + } + } + if ($timeout != 0 && time() > $start_time + $timeout) { + $out = stream_get_contents($pipes[1]); + $err = stream_get_contents($pipes[2]); + + $this->killProperly($handle, $pipes); + + return array("Timeout", $out, $err); + } + + // Since we use non-blocking, the for loop could well take 100% + // CPU. time of 1000 - 10000 seems OK. 100000 slows down the + // program by 50%. + usleep(7000); + } while ($status["running"]); + stream_set_blocking($pipes[1], 1); + stream_set_blocking($pipes[2], 1); + $out .= stream_get_contents($pipes[1]); + $err .= stream_get_contents($pipes[2]); + + $exit_code = $status["exitcode"]; + $this->killProperly($handle, $pipes); + + return array($out, $err, $exit_code); + + } + /** + * Kills a process properly + * + * @param handler &$handle The handler + * @param array &$pipes The pipes + * + * @return void + */ + function killProperly(&$handle, &$pipes) + { + $status = proc_get_status($handle); + + // proc_terminate kills the shell process, but won't kill a runaway infinite + // loop. Get the child processes using ps, before killing the parent. + $ppid = $status["pid"] + 1; + $pids = split("/\s+/", trim(`ps -o pid --no-heading --ppid $ppid`)); + + // if we dont close pipes, we can create deadlock, leaving zombie processes. + foreach ($pipes as &$pipe) { + fclose($pipe); + } + proc_terminate($handle); + proc_close($handle); + + // Not necessarily available. + if (function_exists("posix_kill")) { + foreach ($pids as $pid) { + if (is_numeric($pid)) { + posix_kill($pid, 9); + } + } + } + } + + } $bench = new Benchmark(); <?php class Cachegrindparser { /** * Parses the returning string from a cachegrind call * * @param string $text The text to parse * * @return array An array of the results. */ function parse($text) { $match_names = array ( "instruction" , "instruction_l1_miss" , "instruction_l2_miss" , "data" , "data_read" , "data_write" , "data_l1_miss" , "data_l1_miss_read" , "data_l1_miss_write" , "data_l2_miss" , "data_l2_miss_read" , "data_l2_miss_write" , "l2" , "l2_read" , "l2_write" , "l2_miss" , "l2_miss_read" , "l2_miss_write" , "branch" , "branch_conditional" , "branch_indirect" , "branch_misprediction" , "branch_conditional_misprediction" , "branch_indirect_misprediction"); $start = "^==\d+=="; $middle = ":\s+"; $number = "([0-9,]+)"; $rw = "\s*\(\s*$number rd\s*\+\s*$number wr\)"; $branch_rw = "\s*\(\s*$number cond\s*\+\s*$number ind\)"; $end = "$."; preg_match("/" ."$start I\s+refs$middle$number$end" ."$start I1\s+misses$middle$number$end" ."$start L2i\s+misses$middle$number$end" .".*" ."$start D\s+refs$middle$number$rw$end" ."$start D1\s+misses$middle$number$rw$end" ."$start L2d\s+misses$middle$number$rw$end" .".*" ."$start L2\s+refs$middle$number$rw$end" ."$start L2\s+misses$middle$number$rw$end" .".*" ."$start Branches$middle$number$branch_rw$end" ."$start Mispredicts$middle$number$branch_rw$end" ."/ms", $text, $matches); array_shift($matches); // strip off the // remove commas foreach ($matches as &$match) { $match = preg_replace("/,/", "", $match); } unset($match); // remove the reference from match // make key-value pairs $results = array_combine($match_names, $matches); return $results; } } ?><?php class Smapsparser { var $results; var $usage; function Smapsparser() { $usage = array(); } function parseSmapsData($data) { $lines = explode("\n",$data); $i = 0; $last = array(); $result = array(); $result["objects"] = array(); foreach ($lines as $line) { $matches = array(); if (preg_match('/[a-z0-9]+[-][a-z0-9]+/',$line)) { if (preg_match('/[\d]{2}:[\d]{2}[\s][\d]+[\s]+(?<name>.*)/', $line,$matches)) { if(!empty($last)) $result["objects"][] = $last; $last["name"] = $matches["name"]; } } else if (preg_match('/(?<name>[a-zA-Z_]+):/', $line,$match_type)) { preg_match('/(?<amount>\d+)[\s]kB/', $line, $match_amount); $last[$match_type["name"]] = $match_amount["amount"]; if (!isset($result[$match_type["name"]])) { $result[$match_type["name"]] = 0; } $result[$match_type["name"]] += $match_amount["amount"]; } } $result["objects"][] = $last; $this->usage[] = $result; } function getUsage() { return $this->usage; } /** * Compare elements based on Private_Dirty * */ static function cmpKey($a,$b) { if ($a["Private_Dirty"] == $b["Private_Dirty"]) { return 0; } return ($a["Private_Dirty"] < $b["Private_Dirty"]) ? 1 : -1; } /** * Get element index of peak memory usage * * @return int The index of $this->used's peak */ function getPeak() { $maxi = 0; $maxval = 0; $i = 0; foreach ($this->usage as $mem_array) { if ($maxval < $mem_array["Size"]) { $maxi = $i; $maxval = $mem_array["Size"]; } $i++; } return $maxi; } /** * Clear the memory usage of the parser */ function clear() { unset($results); unset($usage); } /** * Prints the top n memory consuming files based on Dirty Private RSS, * for the maximum peak of memory consumption. * * @param int $n The number of files to be printed * @param string $resource If specified, it will append to this file instead * of stdout * * @return void */ function printMaxUsage($n,$resource = "php://stdout") { $i = 0; if ($resource == "php://stdout") { $fh = fopen($resource, "w"); } else { $fh = fopen($resource, "a+"); } $peak = $this->usage[$this->getPeak()]; $this->sortMemUsage($peak["objects"]); fprintf($fh, "VM Size total: %10skB\n", $peak['Size']); fprintf($fh, "RSS : %10skB Total\n", $peak['Rss']); fprintf($fh, " : %10skB Shared total\n", $peak['Shared_Dirty'] + $peak["Shared_Clean"]); fprintf($fh, " %10skB Private Clean\n", $peak['Private_Clean']); fprintf($fh, " %10skB Private Dirty\n", $peak['Private_Dirty']); fprintf($fh, "%10s %10s %10s %-15s\n", "vm size", "Clean", "Dirty", "File name"); fprintf($fh, "%'-59s\n", "-"); foreach ($peak["objects"] as $key) { $i++; if ($i > $n) { break; } // If it's anonymous, don't print it. if ($key["name"] != "") { fprintf($fh, "%8skB %8skB %8skB %-18s\n", $key['Size'], $key["Private_Clean"], $key["Private_Dirty"], $key["name"]); } else { $i--; } } fprintf($fh, "%s", "\n"); } /** * Sorts the memory usage from the array * * @param &array &$array The of parsed smaps results to be sorted * * @return void */ function sortMemUsage(&$array) { usort($array, "Smapsparser::cmpKey"); } /** * Reads the Smaps data of the given PID * * @param int $pid The PID to read Smaps data from * * @return File contents in form of string or if file not exists * false is returned. */ function readSmapsData($pid) { if (file_exists("/proc/$pid/smaps")) { return file_get_contents("/proc/$pid/smaps"); } return false; } } ?>
« previous php.qa (#65078) next »