diff options
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | bin/.gitignore | 2 | ||||
-rw-r--r-- | config.php | 218 | ||||
-rw-r--r-- | favicon.ico | bin | 0 -> 318 bytes | |||
-rw-r--r-- | index.php | 151 | ||||
-rw-r--r-- | library/FileHandler.php | 376 | ||||
-rw-r--r-- | library/Preprocessor.php | 374 | ||||
-rw-r--r-- | library/Reader.php | 492 | ||||
-rw-r--r-- | library/gprof2dot.py | 1756 | ||||
-rw-r--r-- | library/preprocessor.cpp | 334 | ||||
-rw-r--r-- | makefile | 20 | ||||
-rw-r--r-- | styles/style.css | 101 | ||||
-rw-r--r-- | templates/fileviewer.phtml | 112 | ||||
-rw-r--r-- | templates/index.phtml | 484 |
14 files changed, 2745 insertions, 1678 deletions
@@ -24,6 +24,9 @@ Installation 2. Unzip package to favourite path accessible by webserver. 3. Load webgrind in browser and start profiling +For faster preprocessing under linux, give write access to the `bin` subdirectory +or execute `make` in the unzipped folder (requires GCC). + See the [Installation Wiki page](https://github.com/jokkedk/webgrind/wiki/Installation) for more Credits diff --git a/bin/.gitignore b/bin/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bin/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore @@ -6,98 +6,154 @@ * @author Joakim Nygård */ class Webgrind_Config extends Webgrind_MasterConfig { - /** - * Automatically check if a newer version of webgrind is available for download - */ - static $checkVersion = true; - static $hideWebgrindProfiles = true; - - /** - * Writable dir for information storage. - * If empty, will use system tmp folder or xdebug tmp - */ - static $storageDir = ''; - static $profilerDir = '/tmp'; - - /** - * Suffix for preprocessed files - */ - static $preprocessedSuffix = '.webgrind'; - - static $defaultTimezone = 'Europe/Copenhagen'; - static $dateFormat = 'Y-m-d H:i:s'; - static $defaultCostformat = 'percent'; // 'percent', 'usec' or 'msec' - static $defaultFunctionPercentage = 90; - static $defaultHideInternalFunctions = false; - - /** - * Path to python executable - */ - static $pythonExecutable = '/usr/bin/python'; - - /** - * Path to graphviz dot executable - */ - static $dotExecutable = '/usr/local/bin/dot'; - - /** - * sprintf compatible format for generating links to source files. - * %1$s will be replaced by the full path name of the file - * %2$d will be replaced by the linenumber - */ - static $fileUrlFormat = 'index.php?op=fileviewer&file=%1$s#line%2$d'; // Built in fileviewer - //static $fileUrlFormat = 'txmt://open/?url=file://%1$s&line=%2$d'; // Textmate - //static $fileUrlFormat = 'file://%1$s'; // ? + /** + * Automatically check if a newer version of webgrind is available for download + */ + static $checkVersion = true; + static $hideWebgrindProfiles = true; + + /** + * Writable dir for information storage. + * If empty, will use system tmp folder or xdebug tmp + */ + static $storageDir = ''; + static $profilerDir = '/tmp'; + + /** + * Suffix for preprocessed files + */ + static $preprocessedSuffix = '.webgrind'; + + /** + * Image type of graph to output + * Can be png or svg + */ + static $graphImageType = 'svg'; + + static $defaultTimezone = 'Europe/Copenhagen'; + static $dateFormat = 'Y-m-d H:i:s'; + static $defaultCostformat = 'percent'; // 'percent', 'usec' or 'msec' + static $defaultFunctionPercentage = 90; + static $defaultHideInternalFunctions = false; + + /** + * Path to python executable + */ + static $pythonExecutable = '/usr/bin/python'; + + /** + * Path to graphviz dot executable + */ + static $dotExecutable = '/usr/bin/dot'; + + /** + * sprintf compatible format for generating links to source files. + * %1$s will be replaced by the full path name of the file + * %2$d will be replaced by the linenumber + */ + static $fileUrlFormat = 'index.php?op=fileviewer&file=%1$s#line%2$d'; // Built in fileviewer + //static $fileUrlFormat = 'txmt://open/?url=file://%1$s&line=%2$d'; // Textmate + //static $fileUrlFormat = 'file://%1$s'; // ? /** - * format of the trace drop down list - * default is: invokeurl (tracefile_name) [tracefile_size] - * the following options will be replaced: - * %i - invoked url - * %f - trace file name - * %s - size of trace file - * %m - modified time of file name (in dateFormat specified above) - */ + * format of the trace drop down list + * default is: invokeurl (tracefile_name) [tracefile_size] + * the following options will be replaced: + * %i - invoked url + * %f - trace file name + * %s - size of trace file + * %m - modified time of file name (in dateFormat specified above) + */ static $traceFileListFormat = '%i (%f) [%s]'; + /** + * Proxy functions are stepped over transparently. Functions listed here + * MUST make exactly one (though not necessarily the same one) function + * call per execution. + */ + static $proxyFunctions = array( // resolve dynamic function calls in-place + 'php::call_user_func', + 'php::call_user_func_array', + ); + //static $proxyFunctions = array(); // do not skip any functions + + /** + * Specify which fields display, and the order to display them. Uncomment + * entries to enable, move entries to change order. + */ + static $tableFields = array( + 'Invocation Count', + 'Total Self Cost', + //'Average Self Cost', + 'Total Inclusive Cost', + //'Average Inclusive Cost', + ); ######################### # BELOW NOT FOR EDITING # ######################### - /** - * Regex that matches the trace files generated by xdebug - */ + /** + * Regex that matches the trace files generated by xdebug + */ static function xdebugOutputFormat() { $outputName = ini_get('xdebug.profiler_output_name'); - if($outputName=='') // Ini value not defined - $outputName = '/^cachegrind\.out\..+$/'; - else - $outputName = '/^'.preg_replace('/(%[^%])+/', '.+', $outputName).'$/'; - return $outputName; + if ($outputName=='') // Ini value not defined + $outputName = '/^cachegrind\.out\..+$/'; + else + $outputName = '/^'.preg_replace('/(%[^%])+/', '.+', $outputName).'$/'; + return $outputName; + } + + /** + * Directory to search for trace files + */ + static function xdebugOutputDir() { + $dir = ini_get('xdebug.profiler_output_dir'); + if ($dir=='') // Ini value not defined + return realpath(Webgrind_Config::$profilerDir).'/'; + return realpath($dir).'/'; } - - /** - * Directory to search for trace files - */ - static function xdebugOutputDir() { - $dir = ini_get('xdebug.profiler_output_dir'); - if($dir=='') // Ini value not defined - return realpath(Webgrind_Config::$profilerDir).'/'; - return realpath($dir).'/'; - } - - /** - * Writable dir for information storage - */ - static function storageDir() { - if (!empty(Webgrind_Config::$storageDir)) - return realpath(Webgrind_Config::$storageDir).'/'; - - if (!function_exists('sys_get_temp_dir') || !is_writable(sys_get_temp_dir())) { - # use xdebug setting + + /** + * Writable dir for information storage + */ + static function storageDir() { + if (!empty(Webgrind_Config::$storageDir)) + return realpath(Webgrind_Config::$storageDir).'/'; + + if (!function_exists('sys_get_temp_dir') || !is_writable(sys_get_temp_dir())) { + // use xdebug setting return Webgrind_Config::xdebugOutputDir(); - } - return realpath(sys_get_temp_dir()).'/'; - } + } + return realpath(sys_get_temp_dir()).'/'; + } + + /** + * Binary version of the preprocessor (for faster preprocessing) + * + * If the proper tools are installed and the bin dir is writeable for php, + * automatically compile it (when necessary). + * Automatic compilation disabled if `bin/make-failed` exists. + * Run `make` in the webgrind root directory to manually compile. + */ + static function getBinaryPreprocessor() { + $localBin = __DIR__.'/bin/'; + $makeFailed = $localBin.'make-failed'; + if (is_writable($localBin) && !file_exists($makeFailed)) { + $make = '/usr/bin/make'; + if (is_executable($make)) { + $cwd = getcwd(); + chdir(__DIR__); + exec($make, $output, $retval); + chdir($cwd); + if ($retval != 0) { + touch($makeFailed); + } + } else { + touch($makeFailed); + } + } + return $localBin.'preprocessor'; + } } diff --git a/favicon.ico b/favicon.ico Binary files differnew file mode 100644 index 0000000..26afb58 --- /dev/null +++ b/favicon.ico @@ -4,16 +4,23 @@ * @author Joakim Nygård */ +// Handle static files with PHP built-in webserver +if (PHP_SAPI == 'cli-server') { + if (is_file(realpath(__DIR__ . $_SERVER['REQUEST_URI']))) { + return false; + } +} + class Webgrind_MasterConfig { - static $webgrindVersion = '1.1'; + static $webgrindVersion = '1.3'; } require './config.php'; require './library/FileHandler.php'; // TODO: Errorhandling: -// No files, outputdir not writable +// No files, outputdir not writable set_time_limit(0); @@ -22,13 +29,14 @@ if (ini_get('date.timezone') == '') date_default_timezone_set( Webgrind_Config::$defaultTimezone ); try { - switch(get('op')){ + switch (get('op')) { case 'file_list': - echo json_encode(Webgrind_FileHandler::getInstance()->getTraceList()); + sendJson(Webgrind_FileHandler::getInstance()->getTraceList()); break; + case 'function_list': $dataFile = get('dataFile'); - if($dataFile=='0'){ + if ($dataFile=='0') { $files = Webgrind_FileHandler::getInstance()->getTraceList(); $dataFile = $files[0]['filename']; } @@ -37,47 +45,44 @@ try { $shownTotal = 0; $breakdown = array('internal' => 0, 'procedural' => 0, 'class' => 0, 'include' => 0); - for($i=0;$i<$reader->getFunctionCount();$i++) { + for ($i=0; $i<$reader->getFunctionCount(); $i++) { $functionInfo = $reader->getFunctionInfo($i); - if (false !== strpos($functionInfo['functionName'], 'php::')) { - $breakdown['internal'] += $functionInfo['summedSelfCost']; + $breakdown['internal'] += $functionInfo['summedSelfCostRaw']; $humanKind = 'internal'; - } elseif (false !== strpos($functionInfo['functionName'], 'require_once::') || + } else if (false !== strpos($functionInfo['functionName'], 'require_once::') || false !== strpos($functionInfo['functionName'], 'require::') || false !== strpos($functionInfo['functionName'], 'include_once::') || false !== strpos($functionInfo['functionName'], 'include::')) { - $breakdown['include'] += $functionInfo['summedSelfCost']; + $breakdown['include'] += $functionInfo['summedSelfCostRaw']; $humanKind = 'include'; } else { if (false !== strpos($functionInfo['functionName'], '->') || false !== strpos($functionInfo['functionName'], '::')) { - $breakdown['class'] += $functionInfo['summedSelfCost']; + $breakdown['class'] += $functionInfo['summedSelfCostRaw']; $humanKind = 'class'; } else { - $breakdown['procedural'] += $functionInfo['summedSelfCost']; + $breakdown['procedural'] += $functionInfo['summedSelfCostRaw']; $humanKind = 'procedural'; } } if (!(int)get('hideInternals', 0) || strpos($functionInfo['functionName'], 'php::') === false) { - $shownTotal += $functionInfo['summedSelfCost']; + $shownTotal += $functionInfo['summedSelfCostRaw']; $functions[$i] = $functionInfo; $functions[$i]['nr'] = $i; $functions[$i]['humanKind'] = $humanKind; } - } usort($functions,'costCmp'); $remainingCost = $shownTotal*get('showFraction'); $result['functions'] = array(); - foreach($functions as $function){ - - $remainingCost -= $function['summedSelfCost']; + foreach ($functions as $function) { + $remainingCost -= $function['summedSelfCostRaw']; $function['file'] = urlencode($function['file']); $result['functions'][] = $function; - if($remainingCost<0) + if ($remainingCost<0) break; } $result['summedInvocationCount'] = $reader->getFunctionCount(); @@ -90,9 +95,10 @@ try { $creator = preg_replace('/[^0-9\.]/', '', $reader->getHeader('creator')); $result['linkToFunctionLine'] = version_compare($creator, '2.1') > 0; - - echo json_encode($result); + + sendJson($result); break; + case 'callinfo_list': $reader = Webgrind_FileHandler::getInstance()->getTraceReader(get('file'), get('costFormat', Webgrind_Config::$defaultCostformat)); $functionNr = get('functionNr'); @@ -100,7 +106,7 @@ try { $result = array('calledFrom'=>array(), 'subCalls'=>array()); $foundInvocations = 0; - for($i=0;$i<$function['calledFromInfoCount'];$i++){ + for ($i=0; $i<$function['calledFromInfoCount']; $i++) { $invo = $reader->getCalledFromInfo($functionNr, $i); $foundInvocations += $invo['callCount']; $callerInfo = $reader->getFunctionInfo($invo['functionNr']); @@ -110,53 +116,99 @@ try { } $result['calledByHost'] = ($foundInvocations<$function['invocationCount']); - for($i=0;$i<$function['subCallInfoCount'];$i++){ + for ($i=0; $i<$function['subCallInfoCount']; $i++) { $invo = $reader->getSubCallInfo($functionNr, $i); $callInfo = $reader->getFunctionInfo($invo['functionNr']); $invo['file'] = urlencode($function['file']); // Sub call to $callInfo['file'] but from $function['file'] $invo['callerFunctionName'] = $callInfo['functionName']; $result['subCalls'][] = $invo; } - echo json_encode($result); + sendJson($result); break; + case 'fileviewer': $file = get('file'); - $line = get('line'); - if($file && $file!=''){ + if ($file && $file!='') { $message = ''; - if(!file_exists($file)){ + if (!file_exists($file)) { $message = $file.' does not exist.'; - } else if(!is_readable($file)){ + } else if (!is_readable($file)) { $message = $file.' is not readable.'; - } else if(is_dir($file)){ + } else if (is_dir($file)) { $message = $file.' is a directory.'; } } else { $message = 'No file to view'; } require 'templates/fileviewer.phtml'; - break; + case 'function_graph': $dataFile = get('dataFile'); $showFraction = 100 - intval(get('showFraction') * 100); - if($dataFile == '0'){ + if ($dataFile == '0') { $files = Webgrind_FileHandler::getInstance()->getTraceList(); $dataFile = $files[0]['filename']; } - header("Content-Type: image/png"); - $filename = Webgrind_Config::storageDir().$dataFile.'-'.$showFraction.Webgrind_Config::$preprocessedSuffix.'.png'; - if (!file_exists($filename)) { - shell_exec(Webgrind_Config::$pythonExecutable.' library/gprof2dot.py -n '.$showFraction.' -f callgrind '.Webgrind_Config::xdebugOutputDir().''.escapeshellarg($dataFile).' | '.Webgrind_Config::$dotExecutable.' -Tpng -o ' . escapeshellarg($filename)); - } - readfile($filename); - break; - case 'version_info': - $response = @file_get_contents('http://jokke.dk/webgrindupdate.json?version='.Webgrind_Config::$webgrindVersion); - echo $response; - break; - default: + + $filename = Webgrind_Config::storageDir().$dataFile.'-'.$showFraction.Webgrind_Config::$preprocessedSuffix.'.'.Webgrind_Config::$graphImageType; + if (!file_exists($filename)) { + // Add enclosing quotes if needed + foreach (array('pythonExecutable', 'dotExecutable') as $exe) { + $item =& Webgrind_Config::$$exe; + if (strpos($item, ' ') !== false && !preg_match('/^".+"$/', $item)) { + $item = '"'.$item.'"'; + } + } + shell_exec(Webgrind_Config::$pythonExecutable.' library/gprof2dot.py -n '.$showFraction + .' -f callgrind '.escapeshellarg(Webgrind_Config::xdebugOutputDir().$dataFile).' | ' + .Webgrind_Config::$dotExecutable.' -T'.Webgrind_Config::$graphImageType.' -o '.escapeshellarg($filename)); + } + + if (!file_exists($filename)) { + $file = $filename; + $message = 'Unable to generate <u>'.$file.'</u> via python: <u>'.Webgrind_Config::$pythonExecutable + .'</u> and dot: <u>'.Webgrind_Config::$dotExecutable.'</u>. Please update config.php.'; + require 'templates/fileviewer.phtml'; + break; + } + + if (Webgrind_Config::$graphImageType == 'svg') { + header('Content-Type: image/svg+xml'); + } else { + header('Content-Type: image/'.Webgrind_Config::$graphImageType); + } + readfile($filename); + break; + + case 'version_info': + $response = @file_get_contents('http://alpha0010.github.io/webgrind/webgrindupdate.json?version='.Webgrind_Config::$webgrindVersion); + if ($response) { + header('Content-type: application/json'); + echo $response; + } + break; + + case 'clear_files': + $files = Webgrind_FileHandler::getInstance()->getTraceList(); + if (!$files) { + sendJson(array('done' => 'no files found')); + break; + } + $format = array(); + foreach ($files as $file) { + unlink(Webgrind_Config::xdebugOutputDir().$file['filename']); + $format[] = preg_quote($file['filename'], '/'); + } + $files = preg_grep('/'.implode('|', $format).'/', scandir(Webgrind_Config::storageDir())); + foreach ($files as $file) { + unlink(Webgrind_Config::storageDir().$file); + } + sendJson(array('done' => true)); + break; + + default: $welcome = ''; if (!file_exists(Webgrind_Config::storageDir()) || !is_writable(Webgrind_Config::storageDir())) { $welcome .= 'Webgrind $storageDir does not exist or is not writeable: <code>'.Webgrind_Config::storageDir().'</code><br>'; @@ -171,20 +223,25 @@ try { require 'templates/index.phtml'; } } catch (Exception $e) { - echo json_encode(array('error' => $e->getMessage().'<br>'.$e->getFile().', line '.$e->getLine())); + sendJson(array('error' => $e->getMessage().'<br>'.$e->getFile().', line '.$e->getLine())); return; } -function get($param, $default=false){ +function get($param, $default=false) { return (isset($_GET[$param])? $_GET[$param] : $default); } -function costCmp($a, $b){ - $a = $a['summedSelfCost']; - $b = $b['summedSelfCost']; +function costCmp($a, $b) { + $a = $a['summedSelfCostRaw']; + $b = $b['summedSelfCostRaw']; if ($a == $b) { return 0; } return ($a > $b) ? -1 : 1; } + +function sendJson($object) { + header('Content-type: application/json'); + echo json_encode($object); +} diff --git a/library/FileHandler.php b/library/FileHandler.php index 7bcce61..05c756c 100644 --- a/library/FileHandler.php +++ b/library/FileHandler.php @@ -7,200 +7,200 @@ require 'Preprocessor.php'; * @author Jacob Oettinger * @author Joakim Nygård */ -class Webgrind_FileHandler{ - - private static $singleton = null; - - - /** - * @return Singleton instance of the filehandler - */ - public static function getInstance(){ - if(self::$singleton==null) - self::$singleton = new self(); - return self::$singleton; - } - - private function __construct(){ - // Get list of files matching the defined format - $files = $this->getFiles(Webgrind_Config::xdebugOutputFormat(), Webgrind_Config::xdebugOutputDir()); - - // Get list of preprocessed files +class Webgrind_FileHandler +{ + + private static $singleton = null; + + + /** + * @return Singleton instance of the filehandler + */ + public static function getInstance() { + if (self::$singleton==null) + self::$singleton = new self(); + return self::$singleton; + } + + private function __construct() { + // Get list of files matching the defined format + $files = $this->getFiles(Webgrind_Config::xdebugOutputFormat(), Webgrind_Config::xdebugOutputDir()); + + // Get list of preprocessed files $prepFiles = $this->getPrepFiles('/\\'.Webgrind_Config::$preprocessedSuffix.'$/', Webgrind_Config::storageDir()); - // Loop over the preprocessed files. - foreach($prepFiles as $fileName=>$prepFile){ - $fileName = str_replace(Webgrind_Config::$preprocessedSuffix,'',$fileName); - - // If it is older than its corrosponding original: delete it. - // If it's original does not exist: delete it - if(!isset($files[$fileName]) || $files[$fileName]['mtime']>$prepFile['mtime'] ) - unlink($prepFile['absoluteFilename']); - else - $files[$fileName]['preprocessed'] = true; - } - // Sort by mtime - uasort($files,array($this,'mtimeCmp')); - - $this->files = $files; - } - - /** - * Get the value of the cmd header in $file - * - * @return void string - */ - private function getInvokeUrl($file){ - if (preg_match('/.webgrind$/', $file)) - return 'Webgrind internal'; - - // Grab name of invoked file. - $fp = fopen($file, 'r'); + // Loop over the preprocessed files. + foreach ($prepFiles as $fileName=>$prepFile) { + $fileName = str_replace(Webgrind_Config::$preprocessedSuffix,'',$fileName); + + // If it is older than its corrosponding original: delete it. + // If it's original does not exist: delete it + if (!isset($files[$fileName]) || $files[$fileName]['mtime']>$prepFile['mtime']) + unlink($prepFile['absoluteFilename']); + else + $files[$fileName]['preprocessed'] = true; + } + // Sort by mtime + uasort($files,array($this,'mtimeCmp')); + + $this->files = $files; + } + + /** + * Get the value of the cmd header in $file + * + * @return void string + */ + private function getInvokeUrl($file) { + if (preg_match('/.webgrind$/', $file)) + return 'Webgrind internal'; + + // Grab name of invoked file. + $fp = fopen($file, 'r'); $invokeUrl = ''; - while ((($line = fgets($fp)) !== FALSE) && !strlen($invokeUrl)){ - if (preg_match('/^cmd: (.*)$/', $line, $parts)){ + while ((($line = fgets($fp)) !== FALSE) && !strlen($invokeUrl)) { + if (preg_match('/^cmd: (.*)$/', $line, $parts)) { $invokeUrl = isset($parts[1]) ? $parts[1] : ''; } } fclose($fp); - if (!strlen($invokeUrl)) + if (!strlen($invokeUrl)) $invokeUrl = 'Unknown!'; - return $invokeUrl; - } - - /** - * List of files in $dir whose filename has the format $format - * - * @return array Files - */ - private function getFiles($format, $dir){ - $list = preg_grep($format,scandir($dir)); - $files = array(); - - $scriptFilename = $_SERVER['SCRIPT_FILENAME']; - - # Moved this out of loop to run faster - if (function_exists('xdebug_get_profiler_filename')) - $selfFile = realpath(xdebug_get_profiler_filename()); - else - $selfFile = ''; - - foreach($list as $file){ - $absoluteFilename = $dir.$file; - - // Exclude webgrind preprocessed files - if (false !== strstr($absoluteFilename, Webgrind_Config::$preprocessedSuffix)) - continue; - - // Make sure that script never parses the profile currently being generated. (infinite loop) - if ($selfFile == realpath($absoluteFilename)) - continue; - - $invokeUrl = rtrim($this->getInvokeUrl($absoluteFilename)); - if (Webgrind_Config::$hideWebgrindProfiles && $invokeUrl == dirname(dirname(__FILE__)).DIRECTORY_SEPARATOR.'index.php') - continue; - - - $files[$file] = array('absoluteFilename' => $absoluteFilename, - 'mtime' => filemtime($absoluteFilename), - 'preprocessed' => false, - 'invokeUrl' => $invokeUrl, - 'filesize' => $this->bytestostring(filesize($absoluteFilename)) - ); - } - return $files; - } - - /** - * List of files in $dir whose filename has the format $format - * - * @return array Files - */ - private function getPrepFiles($format, $dir){ - $list = preg_grep($format,scandir($dir)); - $files = array(); - - $scriptFilename = $_SERVER['SCRIPT_FILENAME']; - - foreach($list as $file){ - $absoluteFilename = $dir.$file; - - // Make sure that script does not include the profile currently being generated. (infinite loop) - if (function_exists('xdebug_get_profiler_filename') && realpath(xdebug_get_profiler_filename())==realpath($absoluteFilename)) - continue; - - $files[$file] = array('absoluteFilename' => $absoluteFilename, - 'mtime' => filemtime($absoluteFilename), - 'preprocessed' => true, - 'filesize' => $this->bytestostring(filesize($absoluteFilename)) - ); - } - return $files; - } - /** - * Get list of available trace files. Optionally including traces of the webgrind script it self - * - * @return array Files - */ - public function getTraceList(){ - $result = array(); - foreach($this->files as $fileName=>$file){ - $result[] = array('filename' => $fileName, - 'invokeUrl' => str_replace($_SERVER['DOCUMENT_ROOT'].'/', '', $file['invokeUrl']), - 'filesize' => $file['filesize'], - 'mtime' => date(Webgrind_Config::$dateFormat, $file['mtime']) - ); - } - return $result; - } - - /** - * Get a trace reader for the specific file. - * - * If the file has not been preprocessed yet this will be done first. - * - * @param string File to read - * @param Cost format for the reader - * @return Webgrind_Reader Reader for $file - */ - public function getTraceReader($file, $costFormat){ - $prepFile = Webgrind_Config::storageDir().$file.Webgrind_Config::$preprocessedSuffix; - try{ - $r = new Webgrind_Reader($prepFile, $costFormat); - } catch (Exception $e){ - // Preprocessed file does not exist or other error - Webgrind_Preprocessor::parse(Webgrind_Config::xdebugOutputDir().$file, $prepFile); - $r = new Webgrind_Reader($prepFile, $costFormat); - } - return $r; - } - - /** - * Comparison function for sorting - * - * @return boolean - */ - private function mtimeCmp($a, $b){ - if ($a['mtime'] == $b['mtime']) - return 0; - - return ($a['mtime'] > $b['mtime']) ? -1 : 1; - } - - /** - * Present a size (in bytes) as a human-readable value - * - * @param int $size size (in bytes) - * @param int $precision number of digits after the decimal point - * @return string - */ - private function bytestostring($size, $precision = 0) { - $sizes = array('YB', 'ZB', 'EB', 'PB', 'TB', 'GB', 'MB', 'KB', 'B'); - $total = count($sizes); - - while($total-- && $size > 1024) { - $size /= 1024; - } - return round($size, $precision).$sizes[$total]; + return $invokeUrl; + } + + /** + * List of files in $dir whose filename has the format $format + * + * @return array Files + */ + private function getFiles($format, $dir) { + $list = preg_grep($format,scandir($dir)); + $files = array(); + + $scriptFilename = $_SERVER['SCRIPT_FILENAME']; + + # Moved this out of loop to run faster + if (function_exists('xdebug_get_profiler_filename')) + $selfFile = realpath(xdebug_get_profiler_filename()); + else + $selfFile = ''; + + foreach ($list as $file) { + $absoluteFilename = $dir.$file; + + // Exclude webgrind preprocessed files + if (false !== strstr($absoluteFilename, Webgrind_Config::$preprocessedSuffix)) + continue; + + // Make sure that script never parses the profile currently being generated. (infinite loop) + if ($selfFile == realpath($absoluteFilename)) + continue; + + $invokeUrl = rtrim($this->getInvokeUrl($absoluteFilename)); + if (Webgrind_Config::$hideWebgrindProfiles && $invokeUrl == dirname(dirname(__FILE__)).DIRECTORY_SEPARATOR.'index.php') + continue; + + $files[$file] = array('absoluteFilename' => $absoluteFilename, + 'mtime' => filemtime($absoluteFilename), + 'preprocessed' => false, + 'invokeUrl' => $invokeUrl, + 'filesize' => $this->bytestostring(filesize($absoluteFilename)) + ); + } + return $files; + } + + /** + * List of files in $dir whose filename has the format $format + * + * @return array Files + */ + private function getPrepFiles($format, $dir) { + $list = preg_grep($format,scandir($dir)); + $files = array(); + + $scriptFilename = $_SERVER['SCRIPT_FILENAME']; + + foreach ($list as $file) { + $absoluteFilename = $dir.$file; + + // Make sure that script does not include the profile currently being generated. (infinite loop) + if (function_exists('xdebug_get_profiler_filename') && realpath(xdebug_get_profiler_filename())==realpath($absoluteFilename)) + continue; + + $files[$file] = array('absoluteFilename' => $absoluteFilename, + 'mtime' => filemtime($absoluteFilename), + 'preprocessed' => true, + 'filesize' => $this->bytestostring(filesize($absoluteFilename)) + ); + } + return $files; + } + /** + * Get list of available trace files. Optionally including traces of the webgrind script it self + * + * @return array Files + */ + public function getTraceList() { + $result = array(); + foreach ($this->files as $fileName=>$file) { + $result[] = array('filename' => $fileName, + 'invokeUrl' => str_replace($_SERVER['DOCUMENT_ROOT'].'/', '', $file['invokeUrl']), + 'filesize' => $file['filesize'], + 'mtime' => date(Webgrind_Config::$dateFormat, $file['mtime']) + ); + } + return $result; + } + + /** + * Get a trace reader for the specific file. + * + * If the file has not been preprocessed yet this will be done first. + * + * @param string File to read + * @param Cost format for the reader + * @return Webgrind_Reader Reader for $file + */ + public function getTraceReader($file, $costFormat) { + $prepFile = Webgrind_Config::storageDir().$file.Webgrind_Config::$preprocessedSuffix; + try { + $r = new Webgrind_Reader($prepFile, $costFormat); + } catch (Exception $e) { + // Preprocessed file does not exist or other error + Webgrind_Preprocessor::parse(Webgrind_Config::xdebugOutputDir().$file, $prepFile); + $r = new Webgrind_Reader($prepFile, $costFormat); + } + return $r; + } + + /** + * Comparison function for sorting + * + * @return boolean + */ + private function mtimeCmp($a, $b) { + if ($a['mtime'] == $b['mtime']) + return 0; + + return ($a['mtime'] > $b['mtime']) ? -1 : 1; + } + + /** + * Present a size (in bytes) as a human-readable value + * + * @param int $size size (in bytes) + * @param int $precision number of digits after the decimal point + * @return string + */ + private function bytestostring($size, $precision = 0) { + $sizes = array('YB', 'ZB', 'EB', 'PB', 'TB', 'GB', 'MB', 'KB', 'B'); + $total = count($sizes); + + while ($total-- && $size > 1024) { + $size /= 1024; + } + return round($size, $precision).$sizes[$total]; } -}
\ No newline at end of file +} diff --git a/library/Preprocessor.php b/library/Preprocessor.php index b29581c..7b06aaf 100644 --- a/library/Preprocessor.php +++ b/library/Preprocessor.php @@ -1,161 +1,239 @@ <?php /** * Class for preprocessing callgrind files. - * - * Information from the callgrind file is extracted and written in a binary format for + * + * Information from the callgrind file is extracted and written in a binary format for * fast random access. - * - * @see http://code.google.com/p/webgrind/wiki/PreprocessedFormat + * + * @see https://github.com/jokkedk/webgrind/wiki/Preprocessed-Format * @see http://valgrind.org/docs/manual/cl-format.html * @package Webgrind * @author Jacob Oettinger - **/ + */ class Webgrind_Preprocessor { - /** - * Fileformat version. Embedded in the output for parsers to use. - */ - const FILE_FORMAT_VERSION = 7; - - /** - * Binary number format used. - * @see http://php.net/pack - */ - const NR_FORMAT = 'V'; - - /** - * Size, in bytes, of the above number format - */ - const NR_SIZE = 4; - - /** - * String name of main function - */ - const ENTRY_POINT = '{main}'; - - - /** - * Extract information from $inFile and store in preprocessed form in $outFile - * - * @param string $inFile Callgrind file to read - * @param string $outFile File to write preprocessed data to - * @return void - **/ - static function parse($inFile, $outFile) - { - $in = @fopen($inFile, 'rb'); - if(!$in) - throw new Exception('Could not open '.$inFile.' for reading.'); - $out = @fopen($outFile, 'w+b'); - if(!$out) - throw new Exception('Could not open '.$outFile.' for writing.'); - - $nextFuncNr = 0; - $functions = array(); - $headers = array(); - $calls = array(); - - - // Read information into memory - while(($line = fgets($in))){ - if(substr($line,0,3)==='fl='){ - // Found invocation of function. Read functionname - list($function) = fscanf($in,"fn=%[^\n\r]s"); - if(!isset($functions[$function])){ - $functions[$function] = array( - 'filename' => substr(trim($line),3), - 'invocationCount' => 0, - 'nr' => $nextFuncNr++, - 'count' => 0, - 'summedSelfCost' => 0, - 'summedInclusiveCost' => 0, + /** + * Fileformat version. Embedded in the output for parsers to use. + */ + const FILE_FORMAT_VERSION = 7; + + /** + * Binary number format used. + * @see http://php.net/pack + */ + const NR_FORMAT = 'V'; + + /** + * Size, in bytes, of the above number format + */ + const NR_SIZE = 4; + + /** + * String name of main function + */ + const ENTRY_POINT = '{main}'; + + + /** + * Extract information from $inFile and store in preprocessed form in $outFile + * + * @param string $inFile Callgrind file to read + * @param string $outFile File to write preprocessed data to + * @return void + */ + static function parse($inFile, $outFile) + { + $in = @fopen($inFile, 'rb'); + if (!$in) + throw new Exception('Could not open '.$inFile.' for reading.'); + $out = @fopen($outFile, 'w+b'); + if (!$out) + throw new Exception('Could not open '.$outFile.' for writing.'); + + // If possible, use the binary preprocessor + if (self::binaryParse($inFile, $outFile)) { + return; + } + + $proxyFunctions = array_flip(Webgrind_Config::$proxyFunctions); + $proxyQueue = array(); + $nextFuncNr = 0; + $functionNames = array(); + $functions = array(); + $headers = array(); + + // Read information into memory + while (($line = fgets($in))) { + if (substr($line,0,3)==='fl=') { + // Found invocation of function. Read function name + fscanf($in,"fn=%[^\n\r]s",$function); + $function = self::getCompressedName($function, false); + // Special case for ENTRY_POINT - it contains summary header + if (self::ENTRY_POINT == $function) { + fgets($in); + $headers[] = fgets($in); + fgets($in); + } + // Cost line + fscanf($in,"%d %d",$lnr,$cost); + + if (!isset($functionNames[$function])) { + $index = $nextFuncNr++; + $functionNames[$function] = $index; + if (isset($proxyFunctions[$function])) { + $proxyQueue[$index] = array(); + } + $functions[$index] = array( + 'filename' => self::getCompressedName(substr(trim($line),3), true), + 'line' => $lnr, + 'invocationCount' => 1, + 'summedSelfCost' => $cost, + 'summedInclusiveCost' => $cost, 'calledFromInformation' => array(), 'subCallInformation' => array() - ); - } - $functions[$function]['invocationCount']++; - // Special case for ENTRY_POINT - it contains summary header - if(self::ENTRY_POINT == $function){ - fgets($in); - $headers[] = fgets($in); - fgets($in); - } - // Cost line - list($lnr, $cost) = fscanf($in,"%d %d"); - $functions[$function]['line'] = $lnr; - $functions[$function]['summedSelfCost'] += $cost; - $functions[$function]['summedInclusiveCost'] += $cost; - } else if(substr($line,0,4)==='cfn=') { - - // Found call to function. ($function should contain function call originates from) - $calledFunctionName = substr(trim($line),4); - // Skip call line - fgets($in); - // Cost line - list($lnr, $cost) = fscanf($in,"%d %d"); - - $functions[$function]['summedInclusiveCost'] += $cost; - - if(!isset($functions[$calledFunctionName]['calledFromInformation'][$function.':'.$lnr])) - $functions[$calledFunctionName]['calledFromInformation'][$function.':'.$lnr] = array('functionNr'=>$functions[$function]['nr'],'line'=>$lnr,'callCount'=>0,'summedCallCost'=>0); - - $functions[$calledFunctionName]['calledFromInformation'][$function.':'.$lnr]['callCount']++; - $functions[$calledFunctionName]['calledFromInformation'][$function.':'.$lnr]['summedCallCost'] += $cost; - - if(!isset($functions[$function]['subCallInformation'][$calledFunctionName.':'.$lnr])){ - $functions[$function]['subCallInformation'][$calledFunctionName.':'.$lnr] = array('functionNr'=>$functions[$calledFunctionName]['nr'],'line'=>$lnr,'callCount'=>0,'summedCallCost'=>0); - } - - $functions[$function]['subCallInformation'][$calledFunctionName.':'.$lnr]['callCount']++; - $functions[$function]['subCallInformation'][$calledFunctionName.':'.$lnr]['summedCallCost'] += $cost; - - - } else if(strpos($line,': ')!==false){ - // Found header - $headers[] = $line; - } - } - - - // Write output - $functionCount = sizeof($functions); - fwrite($out, pack(self::NR_FORMAT.'*', self::FILE_FORMAT_VERSION, 0, $functionCount)); - // Make room for function addresses - fseek($out,self::NR_SIZE*$functionCount, SEEK_CUR); - $functionAddresses = array(); - foreach($functions as $functionName => $function){ - $functionAddresses[] = ftell($out); - $calledFromCount = sizeof($function['calledFromInformation']); - $subCallCount = sizeof($function['subCallInformation']); - fwrite($out, pack(self::NR_FORMAT.'*', $function['line'], $function['summedSelfCost'], $function['summedInclusiveCost'], $function['invocationCount'], $calledFromCount, $subCallCount)); - // Write called from information - foreach((array)$function['calledFromInformation'] as $call){ - fwrite($out, pack(self::NR_FORMAT.'*', $call['functionNr'], $call['line'], $call['callCount'], $call['summedCallCost'])); - } - // Write sub call information - foreach((array)$function['subCallInformation'] as $call){ - fwrite($out, pack(self::NR_FORMAT.'*', $call['functionNr'], $call['line'], $call['callCount'], $call['summedCallCost'])); - } - - fwrite($out, $function['filename']."\n".$functionName."\n"); - } - $headersPos = ftell($out); - // Write headers - foreach($headers as $header){ - fwrite($out,$header); - } - - // Write addresses - fseek($out,self::NR_SIZE, SEEK_SET); - fwrite($out, pack(self::NR_FORMAT, $headersPos)); - // Skip function count - fseek($out,self::NR_SIZE, SEEK_CUR); - // Write function addresses - foreach($functionAddresses as $address){ - fwrite($out, pack(self::NR_FORMAT, $address)); - } - - } - + ); + } else { + $index = $functionNames[$function]; + $functions[$index]['invocationCount']++; + $functions[$index]['summedSelfCost'] += $cost; + $functions[$index]['summedInclusiveCost'] += $cost; + } + } else if (substr($line,0,4)==='cfn=') { + // Found call to function. ($function/$index should contain function call originates from) + $calledFunctionName = self::getCompressedName(substr(trim($line),4), false); + // Skip call line + fgets($in); + // Cost line + fscanf($in,"%d %d",$lnr,$cost); + + // Current function is a proxy -> skip + if (isset($proxyQueue[$index])) { + $proxyQueue[$index][] = array( + 'calledIndex' => $functionNames[$calledFunctionName], + 'lnr' => $lnr, + 'cost' => $cost, + ); + continue; + } + + $calledIndex = $functionNames[$calledFunctionName]; + // Called a proxy + if (isset($proxyQueue[$calledIndex])) { + $data = array_shift($proxyQueue[$calledIndex]); + $calledIndex = $data['calledIndex']; + $lnr = $data['lnr']; + $cost = $data['cost']; + } + + $functions[$index]['summedInclusiveCost'] += $cost; + + $key = $index.$lnr; + if (!isset($functions[$calledIndex]['calledFromInformation'][$key])) { + $functions[$calledIndex]['calledFromInformation'][$key] = array('functionNr'=>$index,'line'=>$lnr,'callCount'=>0,'summedCallCost'=>0); + } + + $functions[$calledIndex]['calledFromInformation'][$key]['callCount']++; + $functions[$calledIndex]['calledFromInformation'][$key]['summedCallCost'] += $cost; + + $calledKey = $calledIndex.$lnr; + if (!isset($functions[$index]['subCallInformation'][$calledKey])) { + $functions[$index]['subCallInformation'][$calledKey] = array('functionNr'=>$calledIndex,'line'=>$lnr,'callCount'=>0,'summedCallCost'=>0); + } + + $functions[$index]['subCallInformation'][$calledKey]['callCount']++; + $functions[$index]['subCallInformation'][$calledKey]['summedCallCost'] += $cost; + + } else if (strpos($line,': ')!==false) { + // Found header + $headers[] = $line; + } + } + + $functionNames = array_flip($functionNames); + + // Write output + $functionCount = sizeof($functions); + fwrite($out, pack(self::NR_FORMAT.'*', self::FILE_FORMAT_VERSION, 0, $functionCount)); + // Make room for function addresses + fseek($out,self::NR_SIZE*$functionCount, SEEK_CUR); + $functionAddresses = array(); + foreach ($functions as $index=>$function) { + $functionAddresses[] = ftell($out); + $calledFromCount = sizeof($function['calledFromInformation']); + $subCallCount = sizeof($function['subCallInformation']); + fwrite($out, pack(self::NR_FORMAT.'*', $function['line'], $function['summedSelfCost'], $function['summedInclusiveCost'], $function['invocationCount'], $calledFromCount, $subCallCount)); + // Write called from information + foreach ((array)$function['calledFromInformation'] as $call) { + fwrite($out, pack(self::NR_FORMAT.'*', $call['functionNr'], $call['line'], $call['callCount'], $call['summedCallCost'])); + } + // Write sub call information + foreach ((array)$function['subCallInformation'] as $call) { + fwrite($out, pack(self::NR_FORMAT.'*', $call['functionNr'], $call['line'], $call['callCount'], $call['summedCallCost'])); + } + + fwrite($out, $function['filename']."\n".$functionNames[$index]."\n"); + } + $headersPos = ftell($out); + // Write headers + foreach ($headers as $header) { + fwrite($out,$header); + } + + // Write addresses + fseek($out,self::NR_SIZE, SEEK_SET); + fwrite($out, pack(self::NR_FORMAT, $headersPos)); + // Skip function count + fseek($out,self::NR_SIZE, SEEK_CUR); + // Write function addresses + foreach ($functionAddresses as $address) { + fwrite($out, pack(self::NR_FORMAT, $address)); + } + } + + /** + * Extract information from $inFile and store in preprocessed form in $outFile + * + * @param string $name String to parse (either a filename or function name line) + * @param int $isFile True if this is a filename line (since files and functions have their own symbol tables) + * @return void + **/ + static function getCompressedName($name, $isFile) + { + global $compressedNames; + if (!preg_match("/\((\d+)\)(.+)?/", $name, $matches)) { + return $name; + } + $functionIndex = $matches[1]; + if (isset($matches[2])) { + $compressedNames[$isFile][$functionIndex] = trim($matches[2]); + } else if (!isset($compressedNames[$isFile][$functionIndex])) { + return $name; // should not happen - is file valid? + } + return $compressedNames[$isFile][$functionIndex]; + } + + /** + * Extract information from $inFile and store in preprocessed form in $outFile + * using the (~20x) faster binary preprocessor + * + * @param string $inFile Callgrind file to read + * @param string $outFile File to write preprocessed data to + * @return bool True if binary preprocessor was executed + */ + static function binaryParse($inFile, $outFile) + { + $preprocessor = Webgrind_Config::getBinaryPreprocessor(); + if (!is_executable($preprocessor)) { + return false; + } + + $cmd = escapeshellarg($preprocessor).' '.escapeshellarg($inFile).' '.escapeshellarg($outFile); + foreach (Webgrind_Config::$proxyFunctions as $function) { + $cmd .= ' '.escapeshellarg($function); + } + exec($cmd); + return true; + } + } diff --git a/library/Reader.php b/library/Reader.php index 35c5113..20f12ab 100644 --- a/library/Reader.php +++ b/library/Reader.php @@ -1,269 +1,261 @@ <?php /** * Class for reading datafiles generated by Webgrind_Preprocessor - * + * * @package Webgrind * @author Jacob Oettinger - **/ + */ class Webgrind_Reader { - /** - * File format version that this reader understands - */ - const FILE_FORMAT_VERSION = 7; - - /** - * Binary number format used. - * @see http://php.net/pack - */ - const NR_FORMAT = 'V'; - - /** - * Size, in bytes, of the above number format - */ - const NR_SIZE = 4; - - /** - * Length of a call information block - */ - const CALLINFORMATION_LENGTH = 4; - /** - * Length of a function information block - */ - const FUNCTIONINFORMATION_LENGTH = 6; - + * File format version that this reader understands + */ + const FILE_FORMAT_VERSION = 7; + + /** + * Binary number format used. + * @see http://php.net/pack + */ + const NR_FORMAT = 'V'; + + /** + * Size, in bytes, of the above number format + */ + const NR_SIZE = 4; + + /** + * Length of a call information block + */ + const CALLINFORMATION_LENGTH = 4; + + /** + * Length of a function information block + */ + const FUNCTIONINFORMATION_LENGTH = 6; + + /** + * Address of the headers in the data file + * + * @var int + */ + private $headersPos; + + /** + * Array of addresses pointing to information about functions + * + * @var array + */ + private $functionPos; + + /** + * Array of headers + * + * @var array + */ + private $headers=null; + + /** + * Format to return costs in + * + * @var string + */ + private $costFormat; + + + /** + * Constructor + * @param string Data file to read + * @param string Format to return costs in + */ + function __construct($dataFile, $costFormat) { + $this->fp = @fopen($dataFile,'rb'); + if (!$this->fp) + throw new Exception('Error opening file!'); + + $this->costFormat = $costFormat; + $this->init(); + } + + /** + * Initializes the parser by reading initial information. + * + * Throws an exception if the file version does not match the readers version + * + * @return void + * @throws Exception + */ + private function init() { + list($version, $this->headersPos, $functionCount) = $this->read(3); + if ($version!=self::FILE_FORMAT_VERSION) + throw new Exception('Datafile not correct version. Found '.$version.' expected '.self::FILE_FORMAT_VERSION); + $this->functionPos = $this->read($functionCount); + } + + /** + * Returns number of functions + * @return int + */ + function getFunctionCount(){ + return count($this->functionPos); + } + /** - * Address of the headers in the data file - * - * @var int - */ - private $headersPos; - - /** - * Array of addresses pointing to information about functions - * - * @var array - */ - private $functionPos; - - /** - * Array of headers - * - * @var array - */ - private $headers=null; - - /** - * Format to return costs in - * - * @var string - */ - private $costFormat; - - - /** - * Constructor - * @param string Data file to read - * @param string Format to return costs in - **/ - function __construct($dataFile, $costFormat){ - $this->fp = @fopen($dataFile,'rb'); - if(!$this->fp) - throw new Exception('Error opening file!'); - - $this->costFormat = $costFormat; - $this->init(); - } - - /** - * Initializes the parser by reading initial information. - * - * Throws an exception if the file version does not match the readers version - * - * @return void - * @throws Exception - */ - private function init(){ - list($version, $this->headersPos, $functionCount) = $this->read(3); - if($version!=self::FILE_FORMAT_VERSION) - throw new Exception('Datafile not correct version. Found '.$version.' expected '.self::FILE_FORMAT_VERSION); - $this->functionPos = $this->read($functionCount); - } - - /** - * Returns number of functions - * @return int - */ - function getFunctionCount(){ - return count($this->functionPos); - } - - /** - * Returns information about function with nr $nr - * - * @param $nr int Function number - * @return array Function information - */ - function getFunctionInfo($nr){ - $this->seek($this->functionPos[$nr]); - - list($line, $summedSelfCost, $summedInclusiveCost, $invocationCount, $calledFromCount, $subCallCount) = $this->read(self::FUNCTIONINFORMATION_LENGTH); - - $this->seek(self::NR_SIZE*self::CALLINFORMATION_LENGTH*($calledFromCount+$subCallCount), SEEK_CUR); - $file = $this->readLine(); - $function = $this->readLine(); - - $result = array( - 'file'=>$file, - 'line'=>$line, - 'functionName'=>$function, - 'summedSelfCost'=>$summedSelfCost, - 'summedInclusiveCost'=>$summedInclusiveCost, - 'invocationCount'=>$invocationCount, - 'calledFromInfoCount'=>$calledFromCount, - 'subCallInfoCount'=>$subCallCount - ); + * Returns information about function with nr $nr + * + * @param $nr int Function number + * @return array Function information + */ + function getFunctionInfo($nr) { + $this->seek($this->functionPos[$nr]); + + list($line, $summedSelfCost, $summedInclusiveCost, $invocationCount, $calledFromCount, $subCallCount) = $this->read(self::FUNCTIONINFORMATION_LENGTH); + + $this->seek(self::NR_SIZE*self::CALLINFORMATION_LENGTH*($calledFromCount+$subCallCount), SEEK_CUR); + $file = $this->readLine(); + $function = $this->readLine(); + + $result = array( + 'file' => $file, + 'line' => $line, + 'functionName' => $function, + 'summedSelfCost' => $summedSelfCost, + 'summedInclusiveCost' => $summedInclusiveCost, + 'invocationCount' => $invocationCount, + 'calledFromInfoCount' => $calledFromCount, + 'subCallInfoCount' => $subCallCount + ); + $result['summedSelfCostRaw'] = $result['summedSelfCost']; $result['summedSelfCost'] = $this->formatCost($result['summedSelfCost']); $result['summedInclusiveCost'] = $this->formatCost($result['summedInclusiveCost']); - return $result; - } - - /** - * Returns information about positions where a function has been called from - * - * @param $functionNr int Function number - * @param $calledFromNr int Called from position nr - * @return array Called from information - */ - function getCalledFromInfo($functionNr, $calledFromNr){ - $this->seek( - $this->functionPos[$functionNr] - + self::NR_SIZE - * (self::CALLINFORMATION_LENGTH * $calledFromNr + self::FUNCTIONINFORMATION_LENGTH) - ); - - $data = $this->read(self::CALLINFORMATION_LENGTH); - - $result = array( - 'functionNr'=>$data[0], - 'line'=>$data[1], - 'callCount'=>$data[2], - 'summedCallCost'=>$data[3] - ); - + return $result; + } + + /** + * Returns information about positions where a function has been called from + * + * @param $functionNr int Function number + * @param $calledFromNr int Called from position nr + * @return array Called from information + */ + function getCalledFromInfo($functionNr, $calledFromNr){ + $this->seek( + $this->functionPos[$functionNr] + + self::NR_SIZE + * (self::CALLINFORMATION_LENGTH * $calledFromNr + self::FUNCTIONINFORMATION_LENGTH) + ); + + $data = $this->read(self::CALLINFORMATION_LENGTH); + + $result = array( + 'functionNr' => $data[0], + 'line' => $data[1], + 'callCount' => $data[2], + 'summedCallCost' => $data[3] + ); + $result['summedCallCost'] = $this->formatCost($result['summedCallCost']); - return $result; - } - - /** - * Returns information about functions called by a function - * - * @param $functionNr int Function number - * @param $subCallNr int Sub call position nr - * @return array Sub call information - */ - function getSubCallInfo($functionNr, $subCallNr){ - // Sub call count is the second last number in the FUNCTION_INFORMATION block - $this->seek($this->functionPos[$functionNr] + self::NR_SIZE * (self::FUNCTIONINFORMATION_LENGTH - 2)); - $calledFromInfoCount = $this->read(); - $this->seek( ( ($calledFromInfoCount+$subCallNr) * self::CALLINFORMATION_LENGTH + 1 ) * self::NR_SIZE,SEEK_CUR); - $data = $this->read(self::CALLINFORMATION_LENGTH); - - $result = array( - 'functionNr'=>$data[0], - 'line'=>$data[1], - 'callCount'=>$data[2], - 'summedCallCost'=>$data[3] - ); - + return $result; + } + + /** + * Returns information about functions called by a function + * + * @param $functionNr int Function number + * @param $subCallNr int Sub call position nr + * @return array Sub call information + */ + function getSubCallInfo($functionNr, $subCallNr) { + // Sub call count is the second last number in the FUNCTION_INFORMATION block + $this->seek($this->functionPos[$functionNr] + self::NR_SIZE * (self::FUNCTIONINFORMATION_LENGTH - 2)); + $calledFromInfoCount = $this->read(); + $this->seek( ( ($calledFromInfoCount+$subCallNr) * self::CALLINFORMATION_LENGTH + 1 ) * self::NR_SIZE,SEEK_CUR); + $data = $this->read(self::CALLINFORMATION_LENGTH); + + $result = array( + 'functionNr' => $data[0], + 'line' => $data[1], + 'callCount' => $data[2], + 'summedCallCost' => $data[3] + ); + $result['summedCallCost'] = $this->formatCost($result['summedCallCost']); - return $result; - } - - /** - * Returns array of defined headers - * - * @return array Headers in format array('header name'=>'header value') - */ - function getHeaders(){ - if($this->headers==null){ // Cache headers - $this->seek($this->headersPos); - $this->headers['runs'] = 0; - while($line=$this->readLine()){ - $parts = explode(': ',$line); - if ($parts[0] == 'summary') { - $this->headers['runs']++; - if(isset($this->headers['summary'])) - $this->headers['summary'] += $parts[1]; - else - $this->headers['summary'] = $parts[1]; - } else { + return $result; + } + + /** + * Returns value of a single header + * + * @return string Header value + */ + function getHeader($header) { + if ($this->headers==null) { // Cache headers + $this->seek($this->headersPos); + $this->headers = array( + 'runs' => 0, + 'summary' => '', + 'cmd' => '', + 'creator' => '', + ); + while ($line=$this->readLine()) { + $parts = explode(': ',$line); + if ($parts[0] == 'summary') { + $this->headers['runs']++; + $this->headers['summary'] += $parts[1]; + } else { $this->headers[$parts[0]] = $parts[1]; } - } - } - return $this->headers; - } - - /** - * Returns value of a single header - * - * @return string Header value - */ - function getHeader($header){ - $headers = $this->getHeaders(); - return isset($headers[$header]) ? $headers[$header] : ''; - } - - /** - * Formats $cost using the format in $this->costFormat or optionally the format given as input - * - * @param int $cost Cost - * @param string $format 'percent', 'msec' or 'usec' - * @return int Formatted cost - */ - function formatCost($cost, $format=null) - { - if($format==null) - $format = $this->costFormat; - - if ($format == 'percent') { - $total = $this->getHeader('summary'); - $result = ($total==0) ? 0 : ($cost*100)/$total; - return number_format($result, 2, '.', ''); - } - - if ($format == 'msec') { - return round($cost/1000, 0); - } - - // Default usec - return $cost; - - } - - private function read($numbers=1){ - $values = unpack(self::NR_FORMAT.$numbers,fread($this->fp,self::NR_SIZE*$numbers)); - if($numbers==1) - return $values[1]; - else - return array_values($values); // reindex and return - } - - private function readLine(){ - $result = fgets($this->fp); - if($result) - return trim($result); - else - return $result; - } - - private function seek($offset, $whence=SEEK_SET){ - return fseek($this->fp, $offset, $whence); - } - + } + } + + return $this->headers[$header]; + } + + /** + * Formats $cost using the format in $this->costFormat or optionally the format given as input + * + * @param int $cost Cost + * @param string $format 'percent', 'msec' or 'usec' + * @return int Formatted cost + */ + function formatCost($cost, $format=null) { + if ($format==null) + $format = $this->costFormat; + + if ($format == 'percent') { + $total = $this->getHeader('summary'); + $result = ($total==0) ? 0 : ($cost*100)/$total; + return number_format($result, 2, '.', ''); + } + + if ($format == 'msec') { + return round($cost/1000, 0); + } + + // Default usec + return $cost; + } + + private function read($numbers=1) { + $values = unpack(self::NR_FORMAT.$numbers,fread($this->fp,self::NR_SIZE*$numbers)); + if ($numbers==1) + return $values[1]; + else + return array_values($values); // reindex and return + } + + private function readLine() { + $result = fgets($this->fp); + if ($result) + return trim($result); + else + return $result; + } + + private function seek($offset, $whence=SEEK_SET) { + return fseek($this->fp, $offset, $whence); + } + } diff --git a/library/gprof2dot.py b/library/gprof2dot.py index dc7bc8d..18fa70d 100644 --- a/library/gprof2dot.py +++ b/library/gprof2dot.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright 2008-2009 Jose Fonseca +# Copyright 2008-2014 Jose Fonseca # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published @@ -18,9 +18,7 @@ """Generate a dot graph from the output of several profilers.""" -__author__ = "Jose Fonseca" - -__version__ = "1.0" +__author__ = "Jose Fonseca et al" import sys @@ -30,6 +28,25 @@ import re import textwrap import optparse import xml.parsers.expat +import collections +import locale +import json + + +# Python 2.x/3.x compatibility +if sys.version_info[0] >= 3: + PYTHON_3 = True + def compat_iteritems(x): return x.items() # No iteritems() in Python 3 + def compat_itervalues(x): return x.values() # No itervalues() in Python 3 + def compat_keys(x): return list(x.keys()) # keys() is a generator in Python 3 + basestring = str # No class basestring in Python 3 + unichr = chr # No unichr in Python 3 + xrange = range # No xrange in Python 3 +else: + PYTHON_3 = False + def compat_iteritems(x): return x.iteritems() + def compat_itervalues(x): return x.itervalues() + def compat_keys(x): return x.keys() try: @@ -39,8 +56,16 @@ except ImportError: pass + +######################################################################## +# Model + + +MULTIPLICATION_SIGN = unichr(0xd7) + + def times(x): - return u"%u\xd7" % (x,) + return "%u%s" % (x, MULTIPLICATION_SIGN) def percentage(p): return "%.02f%%" % (p*100.0,) @@ -48,12 +73,6 @@ def percentage(p): def add(a, b): return a + b -def equal(a, b): - if a == b: - return a - else: - return None - def fail(a, b): assert False @@ -79,7 +98,7 @@ def ratio(numerator, denominator): class UndefinedEvent(Exception): """Raised when attempting to get an event which is undefined.""" - + def __init__(self, event): Exception.__init__(self) self.event = event @@ -111,7 +130,7 @@ class Event(object): assert val1 is not None assert val2 is not None return self._aggregator(val1, val2) - + def format(self, val): """Format an event value.""" assert val is not None @@ -119,14 +138,26 @@ class Event(object): CALLS = Event("Calls", 0, add, times) -SAMPLES = Event("Samples", 0, add) -SAMPLES2 = Event("Samples", 0, add) +SAMPLES = Event("Samples", 0, add, times) +SAMPLES2 = Event("Samples", 0, add, times) + +# Count of samples where a given function was either executing or on the stack. +# This is used to calculate the total time ratio according to the +# straightforward method described in Mike Dunlavey's answer to +# stackoverflow.com/questions/1777556/alternatives-to-gprof, item 4 (the myth +# "that recursion is a tricky confusing issue"), last edited 2012-08-30: it's +# just the ratio of TOTAL_SAMPLES over the number of samples in the profile. +# +# Used only when totalMethod == callstacks +TOTAL_SAMPLES = Event("Samples", 0, add, times) TIME = Event("Time", 0.0, add, lambda x: '(' + str(x) + ')') TIME_RATIO = Event("Time ratio", 0.0, add, lambda x: '(' + percentage(x) + ')') TOTAL_TIME = Event("Total time", 0.0, fail) TOTAL_TIME_RATIO = Event("Total time ratio", 0.0, fail, percentage) +totalMethod = 'callratios' + class Object(object): """Base class for all objects in profile which can store events.""" @@ -145,13 +176,13 @@ class Object(object): def __contains__(self, event): return event in self.events - + def __getitem__(self, event): try: return self.events[event] except KeyError: raise UndefinedEvent(event) - + def __setitem__(self, event, value): if value is None: if event in self.events: @@ -162,7 +193,7 @@ class Object(object): class Call(Object): """A call between functions. - + There should be at most one call object for every pair of functions. """ @@ -186,7 +217,7 @@ class Function(Object): self.called = None self.weight = None self.cycle = None - + def add_call(self, call): if call.callee_id in self.calls: sys.stderr.write('warning: overwriting call from function %s to %s\n' % (str(self.id), str(call.callee_id))) @@ -201,6 +232,32 @@ class Function(Object): self.calls[callee_id] = call return self.calls[callee_id] + _parenthesis_re = re.compile(r'\([^()]*\)') + _angles_re = re.compile(r'<[^<>]*>') + _const_re = re.compile(r'\s+const$') + + def stripped_name(self): + """Remove extraneous information from C++ demangled function names.""" + + name = self.name + + # Strip function parameters from name by recursively removing paired parenthesis + while True: + name, n = self._parenthesis_re.subn('', name) + if not n: + break + + # Strip const qualifier + name = self._const_re.sub('', name) + + # Strip template parameters from name by recursively removing paired angles + while True: + name, n = self._angles_re.subn('', name) + if not n: + break + + return name + # TODO: write utility functions def __repr__(self): @@ -212,13 +269,11 @@ class Cycle(Object): def __init__(self): Object.__init__(self) - # XXX: Do cycles need an id? self.functions = set() def add_function(self, function): assert function not in self.functions self.functions.add(function) - # XXX: Aggregate events? if function.cycle is not None: for other in function.cycle.functions: if function not in self.functions: @@ -245,8 +300,8 @@ class Profile(Object): def validate(self): """Validate the edges.""" - for function in self.functions.itervalues(): - for callee_id in function.calls.keys(): + for function in compat_itervalues(self.functions): + for callee_id in compat_keys(function.calls): assert function.calls[callee_id].callee_id == callee_id if callee_id not in self.functions: sys.stderr.write('warning: call to undefined function %s from function %s\n' % (str(callee_id), function.name)) @@ -257,11 +312,11 @@ class Profile(Object): # Apply the Tarjan's algorithm successively until all functions are visited visited = set() - for function in self.functions.itervalues(): + for function in compat_itervalues(self.functions): if function not in visited: self._tarjan(function, 0, [], {}, {}, visited) cycles = [] - for function in self.functions.itervalues(): + for function in compat_itervalues(self.functions): if function.cycle is not None and function.cycle not in cycles: cycles.append(function.cycle) self.cycles = cycles @@ -270,7 +325,54 @@ class Profile(Object): sys.stderr.write("Cycle:\n") for member in cycle.functions: sys.stderr.write("\tFunction %s\n" % member.name) - + + def prune_root(self, root): + visited = set() + frontier = set([root]) + while len(frontier) > 0: + node = frontier.pop() + visited.add(node) + f = self.functions[node] + newNodes = f.calls.keys() + frontier = frontier.union(set(newNodes) - visited) + subtreeFunctions = {} + for n in visited: + subtreeFunctions[n] = self.functions[n] + self.functions = subtreeFunctions + + def prune_leaf(self, leaf): + edgesUp = collections.defaultdict(set) + for f in self.functions.keys(): + for n in self.functions[f].calls.keys(): + edgesUp[n].add(f) + # build the tree up + visited = set() + frontier = set([leaf]) + while len(frontier) > 0: + node = frontier.pop() + visited.add(node) + frontier = frontier.union(edgesUp[node] - visited) + downTree = set(self.functions.keys()) + upTree = visited + path = downTree.intersection(upTree) + pathFunctions = {} + for n in path: + f = self.functions[n] + newCalls = {} + for c in f.calls.keys(): + if c in path: + newCalls[c] = f.calls[c] + f.calls = newCalls + pathFunctions[n] = f + self.functions = pathFunctions + + + def getFunctionId(self, funcName): + for f in self.functions: + if self.functions[f].name == funcName: + return f + return False + def _tarjan(self, function, order, stack, orders, lowlinks, visited): """Tarjan's strongly connected components algorithm. @@ -284,7 +386,7 @@ class Profile(Object): order += 1 pos = len(stack) stack.append(function) - for call in function.calls.itervalues(): + for call in compat_itervalues(function.calls): callee = self.functions[call.callee_id] # TODO: use a set to optimize lookup if callee not in orders: @@ -308,30 +410,43 @@ class Profile(Object): for cycle in self.cycles: cycle_totals[cycle] = 0.0 function_totals = {} - for function in self.functions.itervalues(): + for function in compat_itervalues(self.functions): function_totals[function] = 0.0 - for function in self.functions.itervalues(): - for call in function.calls.itervalues(): + + # Pass 1: function_total gets the sum of call[event] for all + # incoming arrows. Same for cycle_total for all arrows + # that are coming into the *cycle* but are not part of it. + for function in compat_itervalues(self.functions): + for call in compat_itervalues(function.calls): if call.callee_id != function.id: callee = self.functions[call.callee_id] - function_totals[callee] += call[event] - if callee.cycle is not None and callee.cycle is not function.cycle: - cycle_totals[callee.cycle] += call[event] + if event in call.events: + function_totals[callee] += call[event] + if callee.cycle is not None and callee.cycle is not function.cycle: + cycle_totals[callee.cycle] += call[event] + else: + sys.stderr.write("call_ratios: No data for " + function.name + " call to " + callee.name + "\n") - # Compute the ratios - for function in self.functions.itervalues(): - for call in function.calls.itervalues(): + # Pass 2: Compute the ratios. Each call[event] is scaled by the + # function_total of the callee. Calls into cycles use the + # cycle_total, but not calls within cycles. + for function in compat_itervalues(self.functions): + for call in compat_itervalues(function.calls): assert call.ratio is None if call.callee_id != function.id: callee = self.functions[call.callee_id] - if callee.cycle is not None and callee.cycle is not function.cycle: - total = cycle_totals[callee.cycle] + if event in call.events: + if callee.cycle is not None and callee.cycle is not function.cycle: + total = cycle_totals[callee.cycle] + else: + total = function_totals[callee] + call.ratio = ratio(call[event], total) else: - total = function_totals[callee] - call.ratio = ratio(call[event], total) + # Warnings here would only repeat those issued above. + call.ratio = 0.0 def integrate(self, outevent, inevent): - """Propagate function time ratio allong the function calls. + """Propagate function time ratio along the function calls. Must be called after finding the cycles. @@ -341,24 +456,24 @@ class Profile(Object): # Sanity checking assert outevent not in self - for function in self.functions.itervalues(): + for function in compat_itervalues(self.functions): assert outevent not in function assert inevent in function - for call in function.calls.itervalues(): + for call in compat_itervalues(function.calls): assert outevent not in call if call.callee_id != function.id: assert call.ratio is not None - # Aggregate the input for each cycle + # Aggregate the input for each cycle for cycle in self.cycles: total = inevent.null() - for function in self.functions.itervalues(): + for function in compat_itervalues(self.functions): total = inevent.aggregate(total, function[inevent]) self[inevent] = total # Integrate along the edges total = inevent.null() - for function in self.functions.itervalues(): + for function in compat_itervalues(self.functions): total = inevent.aggregate(total, function[inevent]) self._integrate_function(function, outevent, inevent) self[outevent] = total @@ -369,12 +484,12 @@ class Profile(Object): else: if outevent not in function: total = function[inevent] - for call in function.calls.itervalues(): + for call in compat_itervalues(function.calls): if call.callee_id != function.id: total += self._integrate_call(call, outevent, inevent) function[outevent] = total return function[outevent] - + def _integrate_call(self, call, outevent, inevent): assert outevent not in call assert call.ratio is not None @@ -390,29 +505,29 @@ class Profile(Object): total = inevent.null() for member in cycle.functions: subtotal = member[inevent] - for call in member.calls.itervalues(): + for call in compat_itervalues(member.calls): callee = self.functions[call.callee_id] if callee.cycle is not cycle: subtotal += self._integrate_call(call, outevent, inevent) total += subtotal cycle[outevent] = total - + # Compute the time propagated to callers of this cycle callees = {} - for function in self.functions.itervalues(): + for function in compat_itervalues(self.functions): if function.cycle is not cycle: - for call in function.calls.itervalues(): + for call in compat_itervalues(function.calls): callee = self.functions[call.callee_id] if callee.cycle is cycle: try: callees[callee] += call.ratio except KeyError: callees[callee] = call.ratio - + for member in cycle.functions: member[outevent] = outevent.null() - for callee, call_ratio in callees.iteritems(): + for callee, call_ratio in compat_iteritems(callees): ranks = {} call_ratios = {} partials = {} @@ -420,14 +535,14 @@ class Profile(Object): self._call_ratios_cycle(cycle, callee, ranks, call_ratios, set()) partial = self._integrate_cycle_function(cycle, callee, call_ratio, partials, ranks, call_ratios, outevent, inevent) assert partial == max(partials.values()) - assert not total or abs(1.0 - partial/(call_ratio*total)) <= 0.001 + assert abs(call_ratio*total - partial) <= 0.001*call_ratio*total return cycle[outevent] def _rank_cycle_function(self, cycle, function, rank, ranks): if function not in ranks or ranks[function] > rank: ranks[function] = rank - for call in function.calls.itervalues(): + for call in compat_itervalues(function.calls): if call.callee_id != function.id: callee = self.functions[call.callee_id] if callee.cycle is cycle: @@ -436,7 +551,7 @@ class Profile(Object): def _call_ratios_cycle(self, cycle, function, ranks, call_ratios, visited): if function not in visited: visited.add(function) - for call in function.calls.itervalues(): + for call in compat_itervalues(function.calls): if call.callee_id != function.id: callee = self.functions[call.callee_id] if callee.cycle is cycle: @@ -447,7 +562,7 @@ class Profile(Object): def _integrate_cycle_function(self, cycle, function, partial_ratio, partials, ranks, call_ratios, outevent, inevent): if function not in partials: partial = partial_ratio*function[inevent] - for call in function.calls.itervalues(): + for call in compat_itervalues(function.calls): if call.callee_id != function.id: callee = self.functions[call.callee_id] if callee.cycle is not cycle: @@ -474,7 +589,7 @@ class Profile(Object): """Aggregate an event for the whole profile.""" total = event.null() - for function in self.functions.itervalues(): + for function in compat_itervalues(self.functions): try: total = event.aggregate(total, function[event]) except UndefinedEvent: @@ -484,11 +599,11 @@ class Profile(Object): def ratio(self, outevent, inevent): assert outevent not in self assert inevent in self - for function in self.functions.itervalues(): + for function in compat_itervalues(self.functions): assert outevent not in function assert inevent in function function[outevent] = ratio(function[inevent], self[inevent]) - for call in function.calls.itervalues(): + for call in compat_itervalues(function.calls): assert outevent not in call if inevent in call: call[outevent] = ratio(call[inevent], self[inevent]) @@ -498,44 +613,44 @@ class Profile(Object): """Prune the profile""" # compute the prune ratios - for function in self.functions.itervalues(): + for function in compat_itervalues(self.functions): try: function.weight = function[TOTAL_TIME_RATIO] except UndefinedEvent: pass - for call in function.calls.itervalues(): + for call in compat_itervalues(function.calls): callee = self.functions[call.callee_id] if TOTAL_TIME_RATIO in call: # handle exact cases first - call.weight = call[TOTAL_TIME_RATIO] + call.weight = call[TOTAL_TIME_RATIO] else: try: # make a safe estimate - call.weight = min(function[TOTAL_TIME_RATIO], callee[TOTAL_TIME_RATIO]) + call.weight = min(function[TOTAL_TIME_RATIO], callee[TOTAL_TIME_RATIO]) except UndefinedEvent: pass # prune the nodes - for function_id in self.functions.keys(): + for function_id in compat_keys(self.functions): function = self.functions[function_id] if function.weight is not None: if function.weight < node_thres: del self.functions[function_id] # prune the egdes - for function in self.functions.itervalues(): - for callee_id in function.calls.keys(): + for function in compat_itervalues(self.functions): + for callee_id in compat_keys(function.calls): call = function.calls[callee_id] if callee_id not in self.functions or call.weight is not None and call.weight < edge_thres: del function.calls[callee_id] - + def dump(self): - for function in self.functions.itervalues(): + for function in compat_itervalues(self.functions): sys.stderr.write('Function %s:\n' % (function.name,)) self._dump_events(function.events) - for call in function.calls.itervalues(): + for call in compat_itervalues(function.calls): callee = self.functions[call.callee_id] sys.stderr.write(' Call %s:\n' % (callee.name,)) self._dump_events(call.events) @@ -546,10 +661,15 @@ class Profile(Object): sys.stderr.write(' Function %s\n' % (function.name,)) def _dump_events(self, events): - for event, value in events.iteritems(): + for event, value in compat_iteritems(events): sys.stderr.write(' %s: %s\n' % (event.name, event.format(value))) + +######################################################################## +# Parsers + + class Struct: """Masquerade a dictionary with a structure-like behavior.""" @@ -557,7 +677,7 @@ class Struct: if attrs is None: attrs = {} self.__dict__['_attrs'] = attrs - + def __getattr__(self, name): try: return self._attrs[name] @@ -572,12 +692,13 @@ class Struct: def __repr__(self): return repr(self._attrs) - + class ParseError(Exception): """Raised when parsing to signal mismatches.""" def __init__(self, msg, line): + Exception.__init__(self) self.msg = msg # TODO: store more source line information self.line = line @@ -589,31 +710,114 @@ class ParseError(Exception): class Parser: """Parser interface.""" + stdinInput = True + multipleInput = False + def __init__(self): pass def parse(self): raise NotImplementedError - + +class JsonParser(Parser): + """Parser for a custom JSON representation of profile data. + + See schema.json for details. + """ + + + def __init__(self, stream): + Parser.__init__(self) + self.stream = stream + + def parse(self): + + obj = json.load(self.stream) + + assert obj['version'] == 0 + + profile = Profile() + profile[SAMPLES] = 0 + + fns = obj['functions'] + + for functionIndex in range(len(fns)): + fn = fns[functionIndex] + function = Function(functionIndex, fn['name']) + try: + function.module = fn['module'] + except KeyError: + pass + try: + function.process = fn['process'] + except KeyError: + pass + function[SAMPLES] = 0 + profile.add_function(function) + + for event in obj['events']: + callchain = [] + + for functionIndex in event['callchain']: + function = profile.functions[functionIndex] + callchain.append(function) + + cost = event['cost'][0] + + callee = callchain[0] + callee[SAMPLES] += cost + profile[SAMPLES] += cost + + for caller in callchain[1:]: + try: + call = caller.calls[callee.id] + except KeyError: + call = Call(callee.id) + call[SAMPLES2] = cost + caller.add_call(call) + else: + call[SAMPLES2] += cost + + callee = caller + + if False: + profile.dump() + + # compute derived data + profile.validate() + profile.find_cycles() + profile.ratio(TIME_RATIO, SAMPLES) + profile.call_ratios(SAMPLES2) + profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) + + return profile + + class LineParser(Parser): """Base class for parsers that read line-based formats.""" - def __init__(self, file): + def __init__(self, stream): Parser.__init__(self) - self._file = file + self._stream = stream self.__line = None self.__eof = False self.line_no = 0 def readline(self): - line = self._file.readline() + line = self._stream.readline() if not line: self.__line = '' self.__eof = True else: self.line_no += 1 - self.__line = line.rstrip('\r\n') + line = line.rstrip('\r\n') + if not PYTHON_3: + encoding = self._stream.encoding + if encoding is None: + encoding = locale.getpreferredencoding() + line = line.decode(encoding) + self.__line = line def lookahead(self): assert self.__line is not None @@ -664,21 +868,21 @@ class XmlTokenizer: self.index = 0 self.final = False self.skip_ws = skip_ws - + self.character_pos = 0, 0 self.character_data = '' - + self.parser = xml.parsers.expat.ParserCreate() self.parser.StartElementHandler = self.handle_element_start self.parser.EndElementHandler = self.handle_element_end self.parser.CharacterDataHandler = self.handle_character_data - + def handle_element_start(self, name, attributes): self.finish_character_data() line, column = self.pos() token = XmlToken(XML_ELEMENT_START, name, attributes, line, column) self.tokens.append(token) - + def handle_element_end(self, name): self.finish_character_data() line, column = self.pos() @@ -689,15 +893,15 @@ class XmlTokenizer: if not self.character_data: self.character_pos = self.pos() self.character_data += data - + def finish_character_data(self): if self.character_data: - if not self.skip_ws or not self.character_data.isspace(): + if not self.skip_ws or not self.character_data.isspace(): line, column = self.character_pos token = XmlToken(XML_CHARACTER_DATA, self.character_data, None, line, column) self.tokens.append(token) self.character_data = '' - + def next(self): size = 16*1024 while self.index >= len(self.tokens) and not self.final: @@ -705,14 +909,7 @@ class XmlTokenizer: self.index = 0 data = self.fp.read(size) self.final = len(data) < size - try: - self.parser.Parse(data, self.final) - except xml.parsers.expat.ExpatError, e: - #if e.code == xml.parsers.expat.errors.XML_ERROR_NO_ELEMENTS: - if e.code == 3: - pass - else: - raise e + self.parser.Parse(data, self.final) if self.index >= len(self.tokens): line, column = self.pos() token = XmlToken(XML_EOF, None, None, line, column) @@ -728,6 +925,7 @@ class XmlTokenizer: class XmlTokenMismatch(Exception): def __init__(self, expected, found): + Exception.__init__(self) self.expected = expected self.found = found @@ -742,13 +940,13 @@ class XmlParser(Parser): Parser.__init__(self) self.tokenizer = XmlTokenizer(fp) self.consume() - + def consume(self): self.token = self.tokenizer.next() def match_element_start(self, name): return self.token.type == XML_ELEMENT_START and self.token.name_or_data == name - + def match_element_end(self, name): return self.token.type == XML_ELEMENT_END and self.token.name_or_data == name @@ -762,7 +960,7 @@ class XmlParser(Parser): attrs = self.token.attrs self.consume() return attrs - + def element_end(self, name): while self.token.type == XML_CHARACTER_DATA: self.consume() @@ -813,7 +1011,7 @@ class GprofParser(Parser): """Extract a structure from a match object, while translating the types in the process.""" attrs = {} groupdict = mo.groupdict() - for name, value in groupdict.iteritems(): + for name, value in compat_iteritems(groupdict): if value is None: value = None elif self._int_re.match(value): @@ -840,20 +1038,20 @@ class GprofParser(Parser): ) _cg_primary_re = re.compile( - r'^\[(?P<index>\d+)\]?' + - r'\s+(?P<percentage_time>\d+\.\d+)' + - r'\s+(?P<self>\d+\.\d+)' + - r'\s+(?P<descendants>\d+\.\d+)' + - r'\s+(?:(?P<called>\d+)(?:\+(?P<called_self>\d+))?)?' + + r'^\[(?P<index>\d+)\]?' + + r'\s+(?P<percentage_time>\d+\.\d+)' + + r'\s+(?P<self>\d+\.\d+)' + + r'\s+(?P<descendants>\d+\.\d+)' + + r'\s+(?:(?P<called>\d+)(?:\+(?P<called_self>\d+))?)?' + r'\s+(?P<name>\S.*?)' + r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' + r'\s\[(\d+)\]$' ) _cg_parent_re = re.compile( - r'^\s+(?P<self>\d+\.\d+)?' + - r'\s+(?P<descendants>\d+\.\d+)?' + - r'\s+(?P<called>\d+)(?:/(?P<called_total>\d+))?' + + r'^\s+(?P<self>\d+\.\d+)?' + + r'\s+(?P<descendants>\d+\.\d+)?' + + r'\s+(?P<called>\d+)(?:/(?P<called_total>\d+))?' + r'\s+(?P<name>\S.*?)' + r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' + r'\s\[(?P<index>\d+)\]$' @@ -862,19 +1060,19 @@ class GprofParser(Parser): _cg_child_re = _cg_parent_re _cg_cycle_header_re = re.compile( - r'^\[(?P<index>\d+)\]?' + - r'\s+(?P<percentage_time>\d+\.\d+)' + - r'\s+(?P<self>\d+\.\d+)' + - r'\s+(?P<descendants>\d+\.\d+)' + - r'\s+(?:(?P<called>\d+)(?:\+(?P<called_self>\d+))?)?' + + r'^\[(?P<index>\d+)\]?' + + r'\s+(?P<percentage_time>\d+\.\d+)' + + r'\s+(?P<self>\d+\.\d+)' + + r'\s+(?P<descendants>\d+\.\d+)' + + r'\s+(?:(?P<called>\d+)(?:\+(?P<called_self>\d+))?)?' + r'\s+<cycle\s(?P<cycle>\d+)\sas\sa\swhole>' + r'\s\[(\d+)\]$' ) _cg_cycle_member_re = re.compile( - r'^\s+(?P<self>\d+\.\d+)?' + - r'\s+(?P<descendants>\d+\.\d+)?' + - r'\s+(?P<called>\d+)(?:\+(?P<called_self>\d+))?' + + r'^\s+(?P<self>\d+\.\d+)?' + + r'\s+(?P<descendants>\d+\.\d+)?' + + r'\s+(?P<called>\d+)(?:\+(?P<called_self>\d+))?' + r'\s+(?P<name>\S.*?)' + r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' + r'\s\[(?P<index>\d+)\]$' @@ -892,7 +1090,7 @@ class GprofParser(Parser): line = lines.pop(0) if line.startswith('['): break - + # read function parent line mo = self._cg_parent_re.match(line) if not mo: @@ -913,7 +1111,7 @@ class GprofParser(Parser): while lines: line = lines.pop(0) - + # read function subroutine line mo = self._cg_child_re.match(line) if not mo: @@ -923,7 +1121,7 @@ class GprofParser(Parser): else: child = self.translate(mo) children.append(child) - + function.parents = parents function.children = children @@ -948,7 +1146,7 @@ class GprofParser(Parser): continue call = self.translate(mo) cycle.functions.append(call) - + self.cycles[cycle.cycle] = cycle def parse_cg_entry(self, lines): @@ -975,21 +1173,21 @@ class GprofParser(Parser): self.parse_cg_entry(entry_lines) entry_lines = [] else: - entry_lines.append(line) + entry_lines.append(line) line = self.readline() - + def parse(self): self.parse_cg() self.fp.close() profile = Profile() profile[TIME] = 0.0 - + cycles = {} - for index in self.cycles.iterkeys(): + for index in self.cycles: cycles[index] = Cycle() - for entry in self.functions.itervalues(): + for entry in compat_itervalues(self.functions): # populate the function function = Function(entry.index, entry.name) function[TIME] = entry.self @@ -999,16 +1197,16 @@ class GprofParser(Parser): call = Call(entry.index) call[CALLS] = entry.called_self function.called += entry.called_self - + # populate the function calls for child in entry.children: call = Call(child.index) - + assert child.called is not None call[CALLS] = child.called if child.index not in self.functions: - # NOTE: functions that were never called but were discovered by gprof's + # NOTE: functions that were never called but were discovered by gprof's # static call graph analysis dont have a call graph entry so we need # to add them here missing = Function(child.index, child.name) @@ -1024,14 +1222,14 @@ class GprofParser(Parser): try: cycle = cycles[entry.cycle] except KeyError: - sys.stderr.write('warning: <cycle %u as a whole> entry missing\n' % entry.cycle) + sys.stderr.write('warning: <cycle %u as a whole> entry missing\n' % entry.cycle) cycle = Cycle() cycles[entry.cycle] = cycle cycle.add_function(function) profile[TIME] = profile[TIME] + function[TIME] - for cycle in cycles.itervalues(): + for cycle in compat_itervalues(cycles): profile.add_cycle(cycle) # Compute derived events @@ -1044,14 +1242,293 @@ class GprofParser(Parser): return profile +# Clone&hack of GprofParser for VTune Amplifier XE 2013 gprof-cc output. +# Tested only with AXE 2013 for Windows. +# - Use total times as reported by AXE. +# - In the absence of call counts, call ratios are faked from the relative +# proportions of total time. This affects only the weighting of the calls. +# - Different header, separator, and end marker. +# - Extra whitespace after function names. +# - You get a full entry for <spontaneous>, which does not have parents. +# - Cycles do have parents. These are saved but unused (as they are +# for functions). +# - Disambiguated "unrecognized call graph entry" error messages. +# Notes: +# - Total time of functions as reported by AXE passes the val3 test. +# - CPU Time:Children in the input is sometimes a negative number. This +# value goes to the variable descendants, which is unused. +# - The format of gprof-cc reports is unaffected by the use of +# -knob enable-call-counts=true (no call counts, ever), or +# -show-as=samples (results are quoted in seconds regardless). +class AXEParser(Parser): + "Parser for VTune Amplifier XE 2013 gprof-cc report output." + + def __init__(self, fp): + Parser.__init__(self) + self.fp = fp + self.functions = {} + self.cycles = {} + + def readline(self): + line = self.fp.readline() + if not line: + sys.stderr.write('error: unexpected end of file\n') + sys.exit(1) + line = line.rstrip('\r\n') + return line + + _int_re = re.compile(r'^\d+$') + _float_re = re.compile(r'^\d+\.\d+$') + + def translate(self, mo): + """Extract a structure from a match object, while translating the types in the process.""" + attrs = {} + groupdict = mo.groupdict() + for name, value in compat_iteritems(groupdict): + if value is None: + value = None + elif self._int_re.match(value): + value = int(value) + elif self._float_re.match(value): + value = float(value) + attrs[name] = (value) + return Struct(attrs) + + _cg_header_re = re.compile( + '^Index |' + '^-----+ ' + ) + + _cg_footer_re = re.compile(r'^Index\s+Function\s*$') + + _cg_primary_re = re.compile( + r'^\[(?P<index>\d+)\]?' + + r'\s+(?P<percentage_time>\d+\.\d+)' + + r'\s+(?P<self>\d+\.\d+)' + + r'\s+(?P<descendants>\d+\.\d+)' + + r'\s+(?P<name>\S.*?)' + + r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' + + r'\s+\[(\d+)\]' + + r'\s*$' + ) + + _cg_parent_re = re.compile( + r'^\s+(?P<self>\d+\.\d+)?' + + r'\s+(?P<descendants>\d+\.\d+)?' + + r'\s+(?P<name>\S.*?)' + + r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' + + r'(?:\s+\[(?P<index>\d+)\]\s*)?' + + r'\s*$' + ) + + _cg_child_re = _cg_parent_re + + _cg_cycle_header_re = re.compile( + r'^\[(?P<index>\d+)\]?' + + r'\s+(?P<percentage_time>\d+\.\d+)' + + r'\s+(?P<self>\d+\.\d+)' + + r'\s+(?P<descendants>\d+\.\d+)' + + r'\s+<cycle\s(?P<cycle>\d+)\sas\sa\swhole>' + + r'\s+\[(\d+)\]' + + r'\s*$' + ) + + _cg_cycle_member_re = re.compile( + r'^\s+(?P<self>\d+\.\d+)?' + + r'\s+(?P<descendants>\d+\.\d+)?' + + r'\s+(?P<name>\S.*?)' + + r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' + + r'\s+\[(?P<index>\d+)\]' + + r'\s*$' + ) + + def parse_function_entry(self, lines): + parents = [] + children = [] + + while True: + if not lines: + sys.stderr.write('warning: unexpected end of entry\n') + return + line = lines.pop(0) + if line.startswith('['): + break + + # read function parent line + mo = self._cg_parent_re.match(line) + if not mo: + sys.stderr.write('warning: unrecognized call graph entry (1): %r\n' % line) + else: + parent = self.translate(mo) + if parent.name != '<spontaneous>': + parents.append(parent) + + # read primary line + mo = self._cg_primary_re.match(line) + if not mo: + sys.stderr.write('warning: unrecognized call graph entry (2): %r\n' % line) + return + else: + function = self.translate(mo) + + while lines: + line = lines.pop(0) + + # read function subroutine line + mo = self._cg_child_re.match(line) + if not mo: + sys.stderr.write('warning: unrecognized call graph entry (3): %r\n' % line) + else: + child = self.translate(mo) + if child.name != '<spontaneous>': + children.append(child) + + if function.name != '<spontaneous>': + function.parents = parents + function.children = children + + self.functions[function.index] = function + + def parse_cycle_entry(self, lines): + + # Process the parents that were not there in gprof format. + parents = [] + while True: + if not lines: + sys.stderr.write('warning: unexpected end of cycle entry\n') + return + line = lines.pop(0) + if line.startswith('['): + break + mo = self._cg_parent_re.match(line) + if not mo: + sys.stderr.write('warning: unrecognized call graph entry (6): %r\n' % line) + else: + parent = self.translate(mo) + if parent.name != '<spontaneous>': + parents.append(parent) + + # read cycle header line + mo = self._cg_cycle_header_re.match(line) + if not mo: + sys.stderr.write('warning: unrecognized call graph entry (4): %r\n' % line) + return + cycle = self.translate(mo) + + # read cycle member lines + cycle.functions = [] + for line in lines[1:]: + mo = self._cg_cycle_member_re.match(line) + if not mo: + sys.stderr.write('warning: unrecognized call graph entry (5): %r\n' % line) + continue + call = self.translate(mo) + cycle.functions.append(call) + + cycle.parents = parents + self.cycles[cycle.cycle] = cycle + + def parse_cg_entry(self, lines): + if any("as a whole" in linelooper for linelooper in lines): + self.parse_cycle_entry(lines) + else: + self.parse_function_entry(lines) + + def parse_cg(self): + """Parse the call graph.""" + + # skip call graph header + line = self.readline() + while self._cg_header_re.match(line): + line = self.readline() + + # process call graph entries + entry_lines = [] + # An EOF in readline terminates the program without returning. + while not self._cg_footer_re.match(line): + if line.isspace(): + self.parse_cg_entry(entry_lines) + entry_lines = [] + else: + entry_lines.append(line) + line = self.readline() + + def parse(self): + sys.stderr.write('warning: for axe format, edge weights are unreliable estimates derived from function total times.\n') + self.parse_cg() + self.fp.close() + + profile = Profile() + profile[TIME] = 0.0 + + cycles = {} + for index in self.cycles: + cycles[index] = Cycle() + + for entry in compat_itervalues(self.functions): + # populate the function + function = Function(entry.index, entry.name) + function[TIME] = entry.self + function[TOTAL_TIME_RATIO] = entry.percentage_time / 100.0 + + # populate the function calls + for child in entry.children: + call = Call(child.index) + # The following bogus value affects only the weighting of + # the calls. + call[TOTAL_TIME_RATIO] = function[TOTAL_TIME_RATIO] + + if child.index not in self.functions: + # NOTE: functions that were never called but were discovered by gprof's + # static call graph analysis dont have a call graph entry so we need + # to add them here + # FIXME: Is this applicable? + missing = Function(child.index, child.name) + function[TIME] = 0.0 + profile.add_function(missing) + + function.add_call(call) + + profile.add_function(function) + + if entry.cycle is not None: + try: + cycle = cycles[entry.cycle] + except KeyError: + sys.stderr.write('warning: <cycle %u as a whole> entry missing\n' % entry.cycle) + cycle = Cycle() + cycles[entry.cycle] = cycle + cycle.add_function(function) + + profile[TIME] = profile[TIME] + function[TIME] + + for cycle in compat_itervalues(cycles): + profile.add_cycle(cycle) + + # Compute derived events. + profile.validate() + profile.ratio(TIME_RATIO, TIME) + # Lacking call counts, fake call ratios based on total times. + profile.call_ratios(TOTAL_TIME_RATIO) + # The TOTAL_TIME_RATIO of functions is already set. Propagate that + # total time to the calls. (TOTAL_TIME is neither set nor used.) + for function in compat_itervalues(profile.functions): + for call in compat_itervalues(function.calls): + if call.ratio is not None: + callee = profile.functions[call.callee_id] + call[TOTAL_TIME_RATIO] = call.ratio * callee[TOTAL_TIME_RATIO] + + return profile + + class CallgrindParser(LineParser): """Parser for valgrind's callgrind tool. - + See also: - http://valgrind.org/docs/manual/cl-format.html """ - _call_re = re.compile('^calls=\s*(\d+)\s+((\d+|\+\d+|-\d+|\*)\s+)+$') + _call_re = re.compile(r'^calls=\s*(\d+)\s+((\d+|\+\d+|-\d+|\*)\s+)+$') def __init__(self, infile): LineParser.__init__(self, infile) @@ -1078,25 +1555,30 @@ class CallgrindParser(LineParser): self.parse_key('version') self.parse_key('creator') - self.parse_part() + while self.parse_part(): + pass + if not self.eof(): + sys.stderr.write('warning: line %u: unexpected line\n' % self.line_no) + sys.stderr.write('%s\n' % self.lookahead()) # compute derived data self.profile.validate() self.profile.find_cycles() self.profile.ratio(TIME_RATIO, SAMPLES) - self.profile.call_ratios(CALLS) + self.profile.call_ratios(SAMPLES2) self.profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) return self.profile def parse_part(self): + if not self.parse_header_line(): + return False while self.parse_header_line(): pass + if not self.parse_body_line(): + return False while self.parse_body_line(): pass - if not self.eof() and False: - sys.stderr.write('warning: line %u: unexpected line\n' % self.line_no) - sys.stderr.write('%s\n' % self.lookahead()) return True def parse_header_line(self): @@ -1153,7 +1635,7 @@ class CallgrindParser(LineParser): self.parse_association_spec() __subpos_re = r'(0x[0-9a-fA-F]+|\d+|\+\d+|-\d+|\*)' - _cost_re = re.compile(r'^' + + _cost_re = re.compile(r'^' + __subpos_re + r'( +' + __subpos_re + r')*' + r'( +\d+)*' + '$') @@ -1166,7 +1648,16 @@ class CallgrindParser(LineParser): function = self.get_function() - values = line.split(' ') + if calls is None: + # Unlike other aspects, call object (cob) is relative not to the + # last call object, but to the caller's object (ob), so try to + # update it when processing a functions cost line + try: + self.positions['cob'] = self.positions['ob'] + except KeyError: + pass + + values = line.split() assert len(values) <= self.num_positions + self.num_events positions = values[0 : self.num_positions] @@ -1185,25 +1676,25 @@ class CallgrindParser(LineParser): position = int(position) self.last_positions[i] = position - events = map(float, events) + events = [float(event) for event in events] if calls is None: - function[SAMPLES] += events[0] + function[SAMPLES] += events[0] self.profile[SAMPLES] += events[0] else: callee = self.get_callee() callee.called += calls - + try: call = function.calls[callee.id] except KeyError: call = Call(callee.id) call[CALLS] = calls - call[SAMPLES] = events[0] + call[SAMPLES2] = events[0] function.add_call(call) else: call[CALLS] += calls - call[SAMPLES] += events[0] + call[SAMPLES2] += events[0] self.consume() return True @@ -1223,7 +1714,7 @@ class CallgrindParser(LineParser): return True - _position_re = re.compile('^(?P<position>[cj]?(?:ob|fl|fi|fe|fn))=\s*(?:\((?P<id>\d+)\))?(?:\s*(?P<name>.+))?') + _position_re = re.compile(r'^(?P<position>[cj]?(?:ob|fl|fi|fe|fn))=\s*(?:\((?P<id>\d+)\))?(?:\s*(?P<name>.+))?') _position_table_map = { 'ob': 'ob', @@ -1255,7 +1746,7 @@ class CallgrindParser(LineParser): def parse_position_spec(self): line = self.lookahead() - + if line.startswith('jump=') or line.startswith('jcnd='): self.consume() return True @@ -1300,16 +1791,6 @@ class CallgrindParser(LineParser): return None key, value = pair return value - line = self.lookahead() - mo = self._key_re.match(line) - if not mo: - return None - key, value = line.split(':', 1) - if key not in keys: - return None - value = value.strip() - self.consume() - return key, value def parse_keys(self, keys): line = self.lookahead() @@ -1331,6 +1812,8 @@ class CallgrindParser(LineParser): function = self.profile.functions[id] except KeyError: function = Function(id, name) + if module: + function.module = os.path.basename(module) function[SAMPLES] = 0 function.called = 0 self.profile.add_function(function) @@ -1338,20 +1821,144 @@ class CallgrindParser(LineParser): def get_function(self): module = self.positions.get('ob', '') - filename = self.positions.get('fl', '') - function = self.positions.get('fn', '') + filename = self.positions.get('fl', '') + function = self.positions.get('fn', '') return self.make_function(module, filename, function) def get_callee(self): module = self.positions.get('cob', '') - filename = self.positions.get('cfi', '') - function = self.positions.get('cfn', '') + filename = self.positions.get('cfi', '') + function = self.positions.get('cfn', '') return self.make_function(module, filename, function) +class PerfParser(LineParser): + """Parser for linux perf callgraph output. + + It expects output generated with + + perf record -g + perf script | gprof2dot.py --format=perf + """ + + def __init__(self, infile): + LineParser.__init__(self, infile) + self.profile = Profile() + + def readline(self): + # Override LineParser.readline to ignore comment lines + while True: + LineParser.readline(self) + if self.eof() or not self.lookahead().startswith('#'): + break + + def parse(self): + # read lookahead + self.readline() + + profile = self.profile + profile[SAMPLES] = 0 + while not self.eof(): + self.parse_event() + + # compute derived data + profile.validate() + profile.find_cycles() + profile.ratio(TIME_RATIO, SAMPLES) + profile.call_ratios(SAMPLES2) + if totalMethod == "callratios": + # Heuristic approach. TOTAL_SAMPLES is unused. + profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) + elif totalMethod == "callstacks": + # Use the actual call chains for functions. + profile[TOTAL_SAMPLES] = profile[SAMPLES] + profile.ratio(TOTAL_TIME_RATIO, TOTAL_SAMPLES) + # Then propagate that total time to the calls. + for function in compat_itervalues(profile.functions): + for call in compat_itervalues(function.calls): + if call.ratio is not None: + callee = profile.functions[call.callee_id] + call[TOTAL_TIME_RATIO] = call.ratio * callee[TOTAL_TIME_RATIO] + else: + assert False + + return profile + + def parse_event(self): + if self.eof(): + return + + line = self.consume() + assert line + + callchain = self.parse_callchain() + if not callchain: + return + + callee = callchain[0] + callee[SAMPLES] += 1 + self.profile[SAMPLES] += 1 + + for caller in callchain[1:]: + try: + call = caller.calls[callee.id] + except KeyError: + call = Call(callee.id) + call[SAMPLES2] = 1 + caller.add_call(call) + else: + call[SAMPLES2] += 1 + + callee = caller + + # Increment TOTAL_SAMPLES only once on each function. + stack = set(callchain) + for function in stack: + function[TOTAL_SAMPLES] += 1 + + def parse_callchain(self): + callchain = [] + while self.lookahead(): + function = self.parse_call() + if function is None: + break + callchain.append(function) + if self.lookahead() == '': + self.consume() + return callchain + + call_re = re.compile(r'^\s+(?P<address>[0-9a-fA-F]+)\s+(?P<symbol>.*)\s+\((?P<module>[^)]*)\)$') + + def parse_call(self): + line = self.consume() + mo = self.call_re.match(line) + assert mo + if not mo: + return None + + function_name = mo.group('symbol') + if not function_name: + function_name = mo.group('address') + + module = mo.group('module') + + function_id = function_name + ':' + module + + try: + function = self.profile.functions[function_id] + except KeyError: + function = Function(function_id, function_name) + function.module = os.path.basename(module) + function[SAMPLES] = 0 + function[TOTAL_SAMPLES] = 0 + self.profile.add_function(function) + + return function + + class OprofileParser(LineParser): """Parser for oprofile callgraph output. - + See also: - http://oprofile.sourceforge.net/doc/opreport.html#opreport-callgraph """ @@ -1380,16 +1987,16 @@ class OprofileParser(LineParser): self.update_subentries_dict(callers_total, callers) function_total.samples += function.samples self.update_subentries_dict(callees_total, callees) - + def update_subentries_dict(self, totals, partials): - for partial in partials.itervalues(): + for partial in compat_itervalues(partials): try: total = totals[partial.id] except KeyError: totals[partial.id] = partial else: total.samples += partial.samples - + def parse(self): # read lookahead self.readline() @@ -1401,10 +2008,10 @@ class OprofileParser(LineParser): profile = Profile() reverse_call_samples = {} - + # populate the profile profile[SAMPLES] = 0 - for _callers, _function, _callees in self.entries.itervalues(): + for _callers, _function, _callees in compat_itervalues(self.entries): function = Function(_function.id, _function.name) function[SAMPLES] = _function.samples profile.add_function(function) @@ -1416,15 +2023,15 @@ class OprofileParser(LineParser): function.module = os.path.basename(_function.image) total_callee_samples = 0 - for _callee in _callees.itervalues(): + for _callee in compat_itervalues(_callees): total_callee_samples += _callee.samples - for _callee in _callees.itervalues(): + for _callee in compat_itervalues(_callees): if not _callee.self: call = Call(_callee.id) call[SAMPLES2] = _callee.samples function.add_call(call) - + # compute derived data profile.validate() profile.find_cycles() @@ -1510,7 +2117,7 @@ class OprofileParser(LineParser): def match_primary(self): line = self.lookahead() return not line[:1].isspace() - + def match_secondary(self): line = self.lookahead() return line[:1].isspace() @@ -1518,7 +2125,7 @@ class OprofileParser(LineParser): class HProfParser(LineParser): """Parser for java hprof output - + See also: - http://java.sun.com/developer/technicalArticles/Programming/HPROF.html """ @@ -1552,7 +2159,7 @@ class HProfParser(LineParser): functions = {} # build up callgraph - for id, trace in self.traces.iteritems(): + for id, trace in compat_iteritems(self.traces): if not id in self.samples: continue mtime = self.samples[id][0] last = None @@ -1679,9 +2286,9 @@ class SysprofParser(XmlParser): def build_profile(self, objects, nodes): profile = Profile() - + profile[SAMPLES] = 0 - for id, object in objects.iteritems(): + for id, object in compat_iteritems(objects): # Ignore fake objects (process names, modules, "Everything", "kernel", etc.) if object['self'] == 0: continue @@ -1691,7 +2298,7 @@ class SysprofParser(XmlParser): profile.add_function(function) profile[SAMPLES] += function[SAMPLES] - for id, node in nodes.iteritems(): + for id, node in compat_iteritems(nodes): # Ignore fake calls if node['self'] == 0: continue @@ -1734,101 +2341,6 @@ class SysprofParser(XmlParser): return profile -class SharkParser(LineParser): - """Parser for MacOSX Shark output. - - Author: tom@dbservice.com - """ - - def __init__(self, infile): - LineParser.__init__(self, infile) - self.stack = [] - self.entries = {} - - def add_entry(self, function): - try: - entry = self.entries[function.id] - except KeyError: - self.entries[function.id] = (function, { }) - else: - function_total, callees_total = entry - function_total.samples += function.samples - - def add_callee(self, function, callee): - func, callees = self.entries[function.id] - try: - entry = callees[callee.id] - except KeyError: - callees[callee.id] = callee - else: - entry.samples += callee.samples - - def parse(self): - self.readline() - self.readline() - self.readline() - self.readline() - - match = re.compile(r'(?P<prefix>[|+ ]*)(?P<samples>\d+), (?P<symbol>[^,]+), (?P<image>.*)') - - while self.lookahead(): - line = self.consume() - mo = match.match(line) - if not mo: - raise ParseError('failed to parse', line) - - fields = mo.groupdict() - prefix = len(fields.get('prefix', 0)) / 2 - 1 - - symbol = str(fields.get('symbol', 0)) - image = str(fields.get('image', 0)) - - entry = Struct() - entry.id = ':'.join([symbol, image]) - entry.samples = int(fields.get('samples', 0)) - - entry.name = symbol - entry.image = image - - # adjust the callstack - if prefix < len(self.stack): - del self.stack[prefix:] - - if prefix == len(self.stack): - self.stack.append(entry) - - # if the callstack has had an entry, it's this functions caller - if prefix > 0: - self.add_callee(self.stack[prefix - 1], entry) - - self.add_entry(entry) - - profile = Profile() - profile[SAMPLES] = 0 - for _function, _callees in self.entries.itervalues(): - function = Function(_function.id, _function.name) - function[SAMPLES] = _function.samples - profile.add_function(function) - profile[SAMPLES] += _function.samples - - if _function.image: - function.module = os.path.basename(_function.image) - - for _callee in _callees.itervalues(): - call = Call(_callee.id) - call[SAMPLES] = _callee.samples - function.add_call(call) - - # compute derived data - profile.validate() - profile.find_cycles() - profile.ratio(TIME_RATIO, SAMPLES) - profile.call_ratios(SAMPLES) - profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO) - - return profile - - class XPerfParser(Parser): """Parser for CSVs generted by XPerf, from Microsoft Windows Performance Tools. """ @@ -1843,7 +2355,7 @@ class XPerfParser(Parser): def parse(self): import csv reader = csv.reader( - self.stream, + self.stream, delimiter = ',', quotechar = None, escapechar = None, @@ -1851,12 +2363,14 @@ class XPerfParser(Parser): skipinitialspace = True, lineterminator = '\r\n', quoting = csv.QUOTE_NONE) - it = iter(reader) - row = reader.next() - self.parse_header(row) - for row in it: - self.parse_row(row) - + header = True + for row in reader: + if header: + self.parse_header(row) + header = False + else: + self.parse_row(row) + # compute derived data self.profile.validate() self.profile.find_cycles() @@ -1874,7 +2388,7 @@ class XPerfParser(Parser): def parse_row(self, row): fields = {} - for name, column in self.column.iteritems(): + for name, column in compat_iteritems(self.column): value = row[column] for factory in int, float: try: @@ -1884,12 +2398,15 @@ class XPerfParser(Parser): else: break fields[name] = value - + process = fields['Process Name'] symbol = fields['Module'] + '!' + fields['Function'] weight = fields['Weight'] count = fields['Count'] + if process == 'Idle': + return + function = self.get_function(process, symbol) function[SAMPLES] += weight * count self.profile[SAMPLES] += weight * count @@ -1939,6 +2456,8 @@ class SleepyParser(Parser): - http://sleepygraph.sourceforge.net/ """ + stdinInput = False + def __init__(self, filename): Parser.__init__(self) @@ -1950,22 +2469,32 @@ class SleepyParser(Parser): self.calls = {} self.profile = Profile() - + _symbol_re = re.compile( - r'^(?P<id>\w+)' + - r'\s+"(?P<module>[^"]*)"' + - r'\s+"(?P<procname>[^"]*)"' + - r'\s+"(?P<sourcefile>[^"]*)"' + + r'^(?P<id>\w+)' + + r'\s+"(?P<module>[^"]*)"' + + r'\s+"(?P<procname>[^"]*)"' + + r'\s+"(?P<sourcefile>[^"]*)"' + r'\s+(?P<sourceline>\d+)$' ) + def openEntry(self, name): + # Some versions of verysleepy use lowercase filenames + for database_name in self.database.namelist(): + if name.lower() == database_name.lower(): + name = database_name + break + + return self.database.open(name, 'rU') + def parse_symbols(self): - lines = self.database.read('symbols.txt').splitlines() - for line in lines: + for line in self.openEntry('Symbols.txt'): + line = line.decode('UTF-8') + mo = self._symbol_re.match(line) if mo: symbol_id, module, procname, sourcefile, sourceline = mo.groups() - + function_id = ':'.join([module, procname]) try: @@ -1979,10 +2508,11 @@ class SleepyParser(Parser): self.symbols[symbol_id] = function def parse_callstacks(self): - lines = self.database.read("callstacks.txt").splitlines() - for line in lines: + for line in self.openEntry('Callstacks.txt'): + line = line.decode('UTF-8') + fields = line.split() - samples = int(fields[0]) + samples = float(fields[0]) callstack = fields[1:] callstack = [self.symbols[symbol_id] for symbol_id in callstack] @@ -1991,7 +2521,7 @@ class SleepyParser(Parser): callee[SAMPLES] += samples self.profile[SAMPLES] += samples - + for caller in callstack[1:]: try: call = caller.calls[callee.id] @@ -2021,187 +2551,27 @@ class SleepyParser(Parser): return profile -class AQtimeTable: - - def __init__(self, name, fields): - self.name = name - - self.fields = fields - self.field_column = {} - for column in range(len(fields)): - self.field_column[fields[column]] = column - self.rows = [] - - def __len__(self): - return len(self.rows) - - def __iter__(self): - for values, children in self.rows: - fields = {} - for name, value in zip(self.fields, values): - fields[name] = value - children = dict([(child.name, child) for child in children]) - yield fields, children - raise StopIteration - - def add_row(self, values, children=()): - self.rows.append((values, children)) - - -class AQtimeParser(XmlParser): - - def __init__(self, stream): - XmlParser.__init__(self, stream) - self.tables = {} - - def parse(self): - self.element_start('AQtime_Results') - self.parse_headers() - results = self.parse_results() - self.element_end('AQtime_Results') - return self.build_profile(results) - - def parse_headers(self): - self.element_start('HEADERS') - while self.token.type == XML_ELEMENT_START: - self.parse_table_header() - self.element_end('HEADERS') - - def parse_table_header(self): - attrs = self.element_start('TABLE_HEADER') - name = attrs['NAME'] - id = int(attrs['ID']) - field_types = [] - field_names = [] - while self.token.type == XML_ELEMENT_START: - field_type, field_name = self.parse_table_field() - field_types.append(field_type) - field_names.append(field_name) - self.element_end('TABLE_HEADER') - self.tables[id] = name, field_types, field_names - - def parse_table_field(self): - attrs = self.element_start('TABLE_FIELD') - type = attrs['TYPE'] - name = self.character_data() - self.element_end('TABLE_FIELD') - return type, name - - def parse_results(self): - self.element_start('RESULTS') - table = self.parse_data() - self.element_end('RESULTS') - return table - - def parse_data(self): - rows = [] - attrs = self.element_start('DATA') - table_id = int(attrs['TABLE_ID']) - table_name, field_types, field_names = self.tables[table_id] - table = AQtimeTable(table_name, field_names) - while self.token.type == XML_ELEMENT_START: - row, children = self.parse_row(field_types) - table.add_row(row, children) - self.element_end('DATA') - return table - - def parse_row(self, field_types): - row = [None]*len(field_types) - children = [] - self.element_start('ROW') - while self.token.type == XML_ELEMENT_START: - if self.token.name_or_data == 'FIELD': - field_id, field_value = self.parse_field(field_types) - row[field_id] = field_value - elif self.token.name_or_data == 'CHILDREN': - children = self.parse_children() - else: - raise XmlTokenMismatch("<FIELD ...> or <CHILDREN ...>", self.token) - self.element_end('ROW') - return row, children - - def parse_field(self, field_types): - attrs = self.element_start('FIELD') - id = int(attrs['ID']) - type = field_types[id] - value = self.character_data() - if type == 'Integer': - value = int(value) - elif type == 'Float': - value = float(value) - elif type == 'Address': - value = int(value) - elif type == 'String': - pass - else: - assert False - self.element_end('FIELD') - return id, value - - def parse_children(self): - children = [] - self.element_start('CHILDREN') - while self.token.type == XML_ELEMENT_START: - table = self.parse_data() - assert table.name not in children - children.append(table) - self.element_end('CHILDREN') - return children - - def build_profile(self, results): - assert results.name == 'Routines' - profile = Profile() - profile[TIME] = 0.0 - for fields, tables in results: - function = self.build_function(fields) - children = tables['Children'] - for fields, _ in children: - call = self.build_call(fields) - function.add_call(call) - profile.add_function(function) - profile[TIME] = profile[TIME] + function[TIME] - profile[TOTAL_TIME] = profile[TIME] - profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME) - return profile - - def build_function(self, fields): - function = Function(self.build_id(fields), self.build_name(fields)) - function[TIME] = fields['Time'] - function[TOTAL_TIME] = fields['Time with Children'] - #function[TIME_RATIO] = fields['% Time']/100.0 - #function[TOTAL_TIME_RATIO] = fields['% with Children']/100.0 - return function - - def build_call(self, fields): - call = Call(self.build_id(fields)) - call[TIME] = fields['Time'] - call[TOTAL_TIME] = fields['Time with Children'] - #call[TIME_RATIO] = fields['% Time']/100.0 - #call[TOTAL_TIME_RATIO] = fields['% with Children']/100.0 - return call - - def build_id(self, fields): - return ':'.join([fields['Module Name'], fields['Unit Name'], fields['Routine Name']]) - - def build_name(self, fields): - # TODO: use more fields - return fields['Routine Name'] - - class PstatsParser: """Parser python profiling statistics saved with te pstats module.""" + stdinInput = False + multipleInput = True + def __init__(self, *filename): import pstats try: self.stats = pstats.Stats(*filename) except ValueError: + if PYTHON_3: + sys.stderr.write('error: failed to load %s\n' % ', '.join(filename)) + sys.exit(1) import hotshot.stats self.stats = hotshot.stats.load(filename[0]) self.profile = Profile() self.function_ids = {} - def get_function_name(self, (filename, line, name)): + def get_function_name(self, key): + filename, line, name = key module = os.path.splitext(filename)[0] module = os.path.basename(module) return "%s:%d:%s" % (module, line, name) @@ -2222,14 +2592,14 @@ class PstatsParser: def parse(self): self.profile[TIME] = 0.0 self.profile[TOTAL_TIME] = self.stats.total_tt - for fn, (cc, nc, tt, ct, callers) in self.stats.stats.iteritems(): + for fn, (cc, nc, tt, ct, callers) in compat_iteritems(self.stats.stats): callee = self.get_function(fn) callee.called = nc callee[TOTAL_TIME] = ct callee[TIME] = tt self.profile[TIME] += tt self.profile[TOTAL_TIME] = max(self.profile[TOTAL_TIME], ct) - for fn, value in callers.iteritems(): + for fn, value in compat_iteritems(callers): caller = self.get_function(fn) call = Call(callee.id) if isinstance(value, tuple): @@ -2250,8 +2620,10 @@ class PstatsParser: call[TOTAL_TIME] = ratio(value, nc)*ct caller.add_call(call) - #self.stats.print_stats() - #self.stats.print_callees() + + if False: + self.stats.print_stats() + self.stats.print_callees() # Compute derived events self.profile.validate() @@ -2261,13 +2633,34 @@ class PstatsParser: return self.profile +formats = { + "axe": AXEParser, + "callgrind": CallgrindParser, + "hprof": HProfParser, + "json": JsonParser, + "oprofile": OprofileParser, + "perf": PerfParser, + "prof": GprofParser, + "pstats": PstatsParser, + "sleepy": SleepyParser, + "sysprof": SysprofParser, + "xperf": XPerfParser, +} + + +######################################################################## +# Output + + class Theme: - def __init__(self, + def __init__(self, bgcolor = (0.0, 0.0, 1.0), mincolor = (0.0, 0.0, 0.0), maxcolor = (0.0, 0.0, 1.0), fontname = "Arial", + fontcolor = "white", + nodestyle = "filled", minfontsize = 10.0, maxfontsize = 10.0, minpenwidth = 0.5, @@ -2278,6 +2671,8 @@ class Theme: self.mincolor = mincolor self.maxcolor = maxcolor self.fontname = fontname + self.fontcolor = fontcolor + self.nodestyle = nodestyle self.minfontsize = minfontsize self.maxfontsize = maxfontsize self.minpenwidth = minpenwidth @@ -2291,6 +2686,9 @@ class Theme: def graph_fontname(self): return self.fontname + def graph_fontcolor(self): + return self.fontcolor + def graph_fontsize(self): return self.minfontsize @@ -2298,11 +2696,17 @@ class Theme: return self.color(weight) def node_fgcolor(self, weight): - return self.graph_bgcolor() + if self.nodestyle == "filled": + return self.graph_bgcolor() + else: + return self.color(weight) def node_fontsize(self, weight): return self.fontsize(weight) + def node_style(self): + return self.nodestyle + def edge_color(self, weight): return self.color(weight) @@ -2320,10 +2724,10 @@ class Theme: def color(self, weight): weight = min(max(weight, 0.0), 1.0) - + hmin, smin, lmin = self.mincolor hmax, smax, lmax = self.maxcolor - + if self.skew < 0: raise ValueError("Skew must be greater than 0") elif self.skew == 1.0: @@ -2405,6 +2809,35 @@ BW_COLORMAP = Theme( maxpenwidth = 8.0, ) +PRINT_COLORMAP = Theme( + minfontsize = 18.0, + maxfontsize = 30.0, + fontcolor = "black", + nodestyle = "solid", + mincolor = (0.0, 0.0, 0.0), # black + maxcolor = (0.0, 0.0, 0.0), # black + minpenwidth = 0.1, + maxpenwidth = 8.0, +) + + +themes = { + "color": TEMPERATURE_COLORMAP, + "pink": PINK_COLORMAP, + "gray": GRAY_COLORMAP, + "bw": BW_COLORMAP, + "print": PRINT_COLORMAP, +} + + +def sorted_iteritems(d): + # Used mostly for result reproducibility (while testing.) + keys = compat_keys(d) + keys.sort() + for key in keys: + value = d[key] + yield key, value + class DotWriter: """Writer for the DOT language. @@ -2414,31 +2847,64 @@ class DotWriter: http://www.graphviz.org/doc/info/lang.html """ + strip = False + wrap = False + def __init__(self, fp): self.fp = fp + def wrap_function_name(self, name): + """Split the function name on multiple lines.""" + + if len(name) > 32: + ratio = 2.0/3.0 + height = max(int(len(name)/(1.0 - ratio) + 0.5), 1) + width = max(len(name)/height, 32) + # TODO: break lines in symbols + name = textwrap.fill(name, width, break_long_words=False) + + # Take away spaces + name = name.replace(", ", ",") + name = name.replace("> >", ">>") + name = name.replace("> >", ">>") # catch consecutive + + return name + + show_function_events = [TOTAL_TIME_RATIO, TIME_RATIO] + show_edge_events = [TOTAL_TIME_RATIO, CALLS] + def graph(self, profile, theme): self.begin_graph() fontname = theme.graph_fontname() + fontcolor = theme.graph_fontcolor() + nodestyle = theme.node_style() self.attr('graph', fontname=fontname, ranksep=0.25, nodesep=0.125) - self.attr('node', fontname=fontname, shape="box", style="filled", fontcolor="white", width=0, height=0) + self.attr('node', fontname=fontname, shape="box", style=nodestyle, fontcolor=fontcolor, width=0, height=0) self.attr('edge', fontname=fontname) - for function in profile.functions.itervalues(): + for _, function in sorted_iteritems(profile.functions): labels = [] if function.process is not None: labels.append(function.process) if function.module is not None: labels.append(function.module) - labels.append(function.name) - for event in TOTAL_TIME_RATIO, TIME_RATIO: + + if self.strip: + function_name = function.stripped_name() + else: + function_name = function.name + if self.wrap: + function_name = self.wrap_function_name(function_name) + labels.append(function_name) + + for event in self.show_function_events: if event in function.events: label = event.format(function[event]) labels.append(label) if function.called is not None: - labels.append(u"%u\xd7" % (function.called,)) + labels.append("%u%s" % (function.called, MULTIPLICATION_SIGN)) if function.weight is not None: weight = function.weight @@ -2446,18 +2912,18 @@ class DotWriter: weight = 0.0 label = '\n'.join(labels) - self.node(function.id, - label = label, - color = self.color(theme.node_bgcolor(weight)), - fontcolor = self.color(theme.node_fgcolor(weight)), + self.node(function.id, + label = label, + color = self.color(theme.node_bgcolor(weight)), + fontcolor = self.color(theme.node_fgcolor(weight)), fontsize = "%.2f" % theme.node_fontsize(weight), ) - for call in function.calls.itervalues(): + for _, call in sorted_iteritems(function.calls): callee = profile.functions[call.callee_id] labels = [] - for event in TOTAL_TIME_RATIO, CALLS: + for event in self.show_edge_events: if event in call.events: label = event.format(call[event]) labels.append(label) @@ -2471,13 +2937,13 @@ class DotWriter: label = '\n'.join(labels) - self.edge(function.id, call.callee_id, - label = label, - color = self.color(theme.edge_color(weight)), + self.edge(function.id, call.callee_id, + label = label, + color = self.color(theme.edge_color(weight)), fontcolor = self.color(theme.edge_color(weight)), - fontsize = "%.2f" % theme.edge_fontsize(weight), - penwidth = "%.2f" % theme.edge_penwidth(weight), - labeldistance = "%.2f" % theme.edge_penwidth(weight), + fontsize = "%.2f" % theme.edge_fontsize(weight), + penwidth = "%.2f" % theme.edge_penwidth(weight), + labeldistance = "%.2f" % theme.edge_penwidth(weight), arrowsize = "%.2f" % theme.edge_arrowsize(weight), ) @@ -2514,7 +2980,7 @@ class DotWriter: return self.write(' [') first = True - for name, value in attrs.iteritems(): + for name, value in sorted_iteritems(attrs): if first: first = False else: @@ -2536,7 +3002,8 @@ class DotWriter: raise TypeError self.write(s) - def color(self, (r, g, b)): + def color(self, rgb): + r, g, b = rgb def float2int(f): if f <= 0.0: @@ -2548,7 +3015,8 @@ class DotWriter: return "#" + "".join(["%02x" % float2int(c) for c in (r, g, b)]) def escape(self, s): - s = s.encode('utf-8') + if not PYTHON_3: + s = s.encode('utf-8') s = s.replace('\\', r'\\') s = s.replace('\n', r'\n') s = s.replace('\t', r'\t') @@ -2559,205 +3027,159 @@ class DotWriter: self.fp.write(s) -class Main: - """Main program.""" - - themes = { - "color": TEMPERATURE_COLORMAP, - "pink": PINK_COLORMAP, - "gray": GRAY_COLORMAP, - "bw": BW_COLORMAP, - } - - def main(self): - """Main program.""" - - parser = optparse.OptionParser( - usage="\n\t%prog [options] [file] ...", - version="%%prog %s" % __version__) - parser.add_option( - '-o', '--output', metavar='FILE', - type="string", dest="output", - help="output filename [stdout]") - parser.add_option( - '-n', '--node-thres', metavar='PERCENTAGE', - type="float", dest="node_thres", default=0.5, - help="eliminate nodes below this threshold [default: %default]") - parser.add_option( - '-e', '--edge-thres', metavar='PERCENTAGE', - type="float", dest="edge_thres", default=0.1, - help="eliminate edges below this threshold [default: %default]") - parser.add_option( - '-f', '--format', - type="choice", choices=('prof', 'callgrind', 'oprofile', 'hprof', 'sysprof', 'pstats', 'shark', 'sleepy', 'aqtime', 'xperf'), - dest="format", default="prof", - help="profile format: prof, callgrind, oprofile, hprof, sysprof, shark, sleepy, aqtime, pstats, or xperf [default: %default]") - parser.add_option( - '-c', '--colormap', - type="choice", choices=('color', 'pink', 'gray', 'bw'), - dest="theme", default="color", - help="color map: color, pink, gray, or bw [default: %default]") - parser.add_option( - '-s', '--strip', - action="store_true", - dest="strip", default=False, - help="strip function parameters, template parameters, and const modifiers from demangled C++ function names") - parser.add_option( - '-w', '--wrap', - action="store_true", - dest="wrap", default=False, - help="wrap function names") - # add a new option to control skew of the colorization curve - parser.add_option( - '--skew', - type="float", dest="theme_skew", default=1.0, - help="skew the colorization curve. Values < 1.0 give more variety to lower percentages. Value > 1.0 give less variety to lower percentages") - (self.options, self.args) = parser.parse_args(sys.argv[1:]) - - if len(self.args) > 1 and self.options.format != 'pstats': - parser.error('incorrect number of arguments') - - try: - self.theme = self.themes[self.options.theme] - except KeyError: - parser.error('invalid colormap \'%s\'' % self.options.theme) - - # set skew on the theme now that it has been picked. - if self.options.theme_skew: - self.theme.skew = self.options.theme_skew - - if self.options.format == 'prof': - if not self.args: - fp = sys.stdin - else: - fp = open(self.args[0], 'rt') - parser = GprofParser(fp) - elif self.options.format == 'callgrind': - if not self.args: - fp = sys.stdin - else: - fp = open(self.args[0], 'rt') - parser = CallgrindParser(fp) - elif self.options.format == 'oprofile': - if not self.args: - fp = sys.stdin - else: - fp = open(self.args[0], 'rt') - parser = OprofileParser(fp) - elif self.options.format == 'sysprof': - if not self.args: - fp = sys.stdin - else: - fp = open(self.args[0], 'rt') - parser = SysprofParser(fp) - elif self.options.format == 'hprof': - if not self.args: - fp = sys.stdin - else: - fp = open(self.args[0], 'rt') - parser = HProfParser(fp) - elif self.options.format == 'pstats': - if not self.args: - parser.error('at least a file must be specified for pstats input') - parser = PstatsParser(*self.args) - elif self.options.format == 'xperf': - if not self.args: - fp = sys.stdin - else: - fp = open(self.args[0], 'rt') - parser = XPerfParser(fp) - elif self.options.format == 'shark': - if not self.args: - fp = sys.stdin - else: - fp = open(self.args[0], 'rt') - parser = SharkParser(fp) - elif self.options.format == 'sleepy': - if len(self.args) != 1: - parser.error('exactly one file must be specified for sleepy input') - parser = SleepyParser(self.args[0]) - elif self.options.format == 'aqtime': - if not self.args: - fp = sys.stdin - else: - fp = open(self.args[0], 'rt') - parser = AQtimeParser(fp) - else: - parser.error('invalid format \'%s\'' % self.options.format) - - self.profile = parser.parse() - - if self.options.output is None: - self.output = sys.stdout - else: - self.output = open(self.options.output, 'wt') - - self.write_graph() - - _parenthesis_re = re.compile(r'\([^()]*\)') - _angles_re = re.compile(r'<[^<>]*>') - _const_re = re.compile(r'\s+const$') - def strip_function_name(self, name): - """Remove extraneous information from C++ demangled function names.""" +######################################################################## +# Main program - # Strip function parameters from name by recursively removing paired parenthesis - while True: - name, n = self._parenthesis_re.subn('', name) - if not n: - break - # Strip const qualifier - name = self._const_re.sub('', name) +def naturalJoin(values): + if len(values) >= 2: + return ', '.join(values[:-1]) + ' or ' + values[-1] - # Strip template parameters from name by recursively removing paired angles - while True: - name, n = self._angles_re.subn('', name) - if not n: - break + else: + return ''.join(values) - return name - def wrap_function_name(self, name): - """Split the function name on multiple lines.""" +def main(): + """Main program.""" - if len(name) > 32: - ratio = 2.0/3.0 - height = max(int(len(name)/(1.0 - ratio) + 0.5), 1) - width = max(len(name)/height, 32) - # TODO: break lines in symbols - name = textwrap.fill(name, width, break_long_words=False) + global totalMethod + + formatNames = list(formats.keys()) + formatNames.sort() + + optparser = optparse.OptionParser( + usage="\n\t%prog [options] [file] ...") + optparser.add_option( + '-o', '--output', metavar='FILE', + type="string", dest="output", + help="output filename [stdout]") + optparser.add_option( + '-n', '--node-thres', metavar='PERCENTAGE', + type="float", dest="node_thres", default=0.5, + help="eliminate nodes below this threshold [default: %default]") + optparser.add_option( + '-e', '--edge-thres', metavar='PERCENTAGE', + type="float", dest="edge_thres", default=0.1, + help="eliminate edges below this threshold [default: %default]") + optparser.add_option( + '-f', '--format', + type="choice", choices=formatNames, + dest="format", default="prof", + help="profile format: %s [default: %%default]" % naturalJoin(formatNames)) + optparser.add_option( + '--total', + type="choice", choices=('callratios', 'callstacks'), + dest="totalMethod", default=totalMethod, + help="preferred method of calculating total time: callratios or callstacks (currently affects only perf format) [default: %default]") + optparser.add_option( + '-c', '--colormap', + type="choice", choices=('color', 'pink', 'gray', 'bw', 'print'), + dest="theme", default="color", + help="color map: color, pink, gray, bw, or print [default: %default]") + optparser.add_option( + '-s', '--strip', + action="store_true", + dest="strip", default=False, + help="strip function parameters, template parameters, and const modifiers from demangled C++ function names") + optparser.add_option( + '-w', '--wrap', + action="store_true", + dest="wrap", default=False, + help="wrap function names") + optparser.add_option( + '--show-samples', + action="store_true", + dest="show_samples", default=False, + help="show function samples") + # add option to create subtree or show paths + optparser.add_option( + '-z', '--root', + type="string", + dest="root", default="", + help="prune call graph to show only descendants of specified root function") + optparser.add_option( + '-l', '--leaf', + type="string", + dest="leaf", default="", + help="prune call graph to show only ancestors of specified leaf function") + # add a new option to control skew of the colorization curve + optparser.add_option( + '--skew', + type="float", dest="theme_skew", default=1.0, + help="skew the colorization curve. Values < 1.0 give more variety to lower percentages. Values > 1.0 give less variety to lower percentages") + (options, args) = optparser.parse_args(sys.argv[1:]) + + if len(args) > 1 and options.format != 'pstats': + optparser.error('incorrect number of arguments') - # Take away spaces - name = name.replace(", ", ",") - name = name.replace("> >", ">>") - name = name.replace("> >", ">>") # catch consecutive + try: + theme = themes[options.theme] + except KeyError: + optparser.error('invalid colormap \'%s\'' % options.theme) - return name + # set skew on the theme now that it has been picked. + if options.theme_skew: + theme.skew = options.theme_skew - def compress_function_name(self, name): - """Compress function name according to the user preferences.""" + totalMethod = options.totalMethod - if self.options.strip: - name = self.strip_function_name(name) + try: + Format = formats[options.format] + except KeyError: + optparser.error('invalid format \'%s\'' % options.format) + + if Format.stdinInput: + if not args: + fp = sys.stdin + elif PYTHON_3: + fp = open(args[0], 'rt', encoding='UTF-8') + else: + fp = open(args[0], 'rt') + parser = Format(fp) + elif Format.multipleInput: + if not args: + optparser.error('at least a file must be specified for %s input' % options.format) + parser = Format(*args) + else: + if len(args) != 1: + optparser.error('exactly one file must be specified for %s input' % options.format) + parser = Format(args[0]) - if self.options.wrap: - name = self.wrap_function_name(name) + profile = parser.parse() - # TODO: merge functions with same resulting name + if options.output is None: + output = sys.stdout + else: + if PYTHON_3: + output = open(options.output, 'wt', encoding='UTF-8') + else: + output = open(options.output, 'wt') - return name + dot = DotWriter(output) + dot.strip = options.strip + dot.wrap = options.wrap + if options.show_samples: + dot.show_function_events.append(SAMPLES) - def write_graph(self): - dot = DotWriter(self.output) - profile = self.profile - profile.prune(self.options.node_thres/100.0, self.options.edge_thres/100.0) + profile = profile + profile.prune(options.node_thres/100.0, options.edge_thres/100.0) - for function in profile.functions.itervalues(): - function.name = self.compress_function_name(function.name) + if options.root: + rootId = profile.getFunctionId(options.root) + if not rootId: + sys.stderr.write('root node ' + options.root + ' not found (might already be pruned : try -e0 -n0 flags)\n') + sys.exit(1) + profile.prune_root(rootId) + if options.leaf: + leafId = profile.getFunctionId(options.leaf) + if not leafId: + sys.stderr.write('leaf node ' + options.leaf + ' not found (maybe already pruned : try -e0 -n0 flags)\n') + sys.exit(1) + profile.prune_leaf(leafId) - dot.graph(profile, self.theme) + dot.graph(profile, theme) if __name__ == '__main__': - Main().main()
\ No newline at end of file + main() diff --git a/library/preprocessor.cpp b/library/preprocessor.cpp new file mode 100644 index 0000000..31614f3 --- /dev/null +++ b/library/preprocessor.cpp @@ -0,0 +1,334 @@ +/** + * This is ported from Preprocessor.php for performance. + */ + +// #include <Winsock2.h> +#include <arpa/inet.h> +#include <algorithm> +#include <fstream> +#include <map> +#include <queue> +#include <string> +#include <vector> + +/** + * Fileformat version. Embedded in the output for parsers to use. + */ +#define FILE_FORMAT_VERSION 7 + +// NR_FORMAT = 'V' - unsigned long 32 bit little endian + +/** + * Size, in bytes, of the above number format + */ +#define NR_SIZE 4 + +/** + * String name of main function + */ +#define ENTRY_POINT "{main}" + +struct ProxyData +{ + ProxyData(int _calledIndex, int _lnr, int _cost) : + calledIndex(_calledIndex), lnr(_lnr), cost(_cost) + {} + + int calledIndex; + int lnr; + int cost; +}; + +struct CallData +{ + CallData(int _functionNr, int _line) : + functionNr(_functionNr), line(_line), callCount(0), summedCallCost(0) + {} + + int functionNr; + int line; + int callCount; + int summedCallCost; +}; + +inline CallData& insertGetOrderedMap(int functionNr, int line, std::map<int, size_t>& keyMap, std::vector<CallData>& data) +{ + int key = functionNr ^ (line << 16) ^ (line >> 16); + std::map<int, size_t>::iterator kmItr = keyMap.find(key); + if (kmItr != keyMap.end()) { + return data[kmItr->second]; + } + keyMap[key] = data.size(); + data.push_back(CallData(functionNr, line)); + return data.back(); +} + +struct FunctionData +{ + FunctionData(const std::string& _filename, int _line, int _cost) : + filename(_filename), + line(_line), + invocationCount(1), + summedSelfCost(_cost), + summedInclusiveCost(_cost) + {} + + std::string filename; + int line; + int invocationCount; + int summedSelfCost; + int summedInclusiveCost; + std::vector<CallData> calledFromInformation; + std::vector<CallData> subCallInformation; + + CallData& getCalledFromData(int _functionNr, int _line) + { + return insertGetOrderedMap(_functionNr, _line, calledFromMap, calledFromInformation); + } + + CallData& getSubCallData(int _functionNr, int _line) + { + return insertGetOrderedMap(_functionNr, _line, subCallMap, subCallInformation); + } + +private: + std::map<int, size_t> calledFromMap; + std::map<int, size_t> subCallMap; +}; + +class Webgrind_Preprocessor +{ +public: + + /** + * Extract information from inFile and store in preprocessed form in outFile + * + * @param inFile Callgrind file to read + * @param outFile File to write preprocessed data to + * @param proxyFunctions Functions to skip, treated as proxies + */ + void parse(const char* inFile, const char* outFile, std::vector<std::string>& proxyFunctions) + { + std::ifstream in(inFile); + std::ofstream out(outFile, std::ios::out | std::ios::binary | std::ios::trunc); + + std::map< int, std::queue<ProxyData> > proxyQueue; + int nextFuncNr = 0; + std::map<std::string, int> functionNames; + std::vector<FunctionData> functions; + std::vector<std::string> headers; + + std::string line; + std::string buffer; + int lnr; + int cost; + int index; + + // Read information into memory + while (std::getline(in, line)) { + if (line.compare(0, 3, "fl=") == 0) { + // Found invocation of function. Read function name + std::string function; + std::getline(in, function); + function.erase(0, 3); + getCompressedName(function, false); + // Special case for ENTRY_POINT - it contains summary header + if (function == ENTRY_POINT) { + std::getline(in, buffer); + std::getline(in, buffer); + headers.push_back(buffer); + std::getline(in, buffer); + } + // Cost line + in >> lnr >> cost; + std::getline(in, buffer); + + std::map<std::string, int>::const_iterator fnItr = functionNames.find(function); + if (fnItr == functionNames.end()) { + index = nextFuncNr++; + functionNames[function] = index; + if (std::binary_search(proxyFunctions.begin(), proxyFunctions.end(), function)) { + proxyQueue[index]; + } + line.erase(0, 3); + getCompressedName(line, true); + functions.push_back(FunctionData(line, lnr, cost)); + } else { + index = fnItr->second; + FunctionData& funcData = functions[index]; + funcData.invocationCount++; + funcData.summedSelfCost += cost; + funcData.summedInclusiveCost += cost; + } + } else if (line.compare(0, 4, "cfn=") == 0) { + // Found call to function. ($function/$index should contain function call originates from) + line.erase(0, 4); + getCompressedName(line, false); // calledFunctionName + // Skip call line + std::getline(in, buffer); + // Cost line + in >> lnr >> cost; + std::getline(in, buffer); + + int calledIndex = functionNames[line]; + + // Current function is a proxy -> skip + std::map< int, std::queue<ProxyData> >::iterator pqItr = proxyQueue.find(index); + if (pqItr != proxyQueue.end()) { + pqItr->second.push(ProxyData(calledIndex, lnr, cost)); + continue; + } + + // Called a proxy + pqItr = proxyQueue.find(calledIndex); + if (pqItr != proxyQueue.end()) { + ProxyData& data = pqItr->second.front(); + calledIndex = data.calledIndex; + lnr = data.lnr; + cost = data.cost; + pqItr->second.pop(); + } + + functions[index].summedInclusiveCost += cost; + + CallData& calledFromData = functions[calledIndex].getCalledFromData(index, lnr); + + calledFromData.callCount++; + calledFromData.summedCallCost += cost; + + CallData& subCallData = functions[index].getSubCallData(calledIndex, lnr); + + subCallData.callCount++; + subCallData.summedCallCost += cost; + + } else if (line.find(": ") != std::string::npos) { + // Found header + headers.push_back(line); + } + } + in.close(); + + std::vector<std::string> reFunctionNames(functionNames.size()); + for (std::map<std::string, int>::const_iterator fnItr = functionNames.begin(); + fnItr != functionNames.end(); ++fnItr) { + reFunctionNames[fnItr->second] = fnItr->first; + } + + // Write output + std::vector<uint32_t> writeBuff; + writeBuff.push_back(FILE_FORMAT_VERSION); + writeBuff.push_back(0); + writeBuff.push_back(functions.size()); + writeBuffer(out, writeBuff); + // Make room for function addresses + out.seekp(NR_SIZE * functions.size(), std::ios::cur); + std::vector<uint32_t> functionAddresses; + for (size_t index = 0; index < functions.size(); ++index) { + functionAddresses.push_back(out.tellp()); + FunctionData& function = functions[index]; + writeBuff.push_back(function.line); + writeBuff.push_back(function.summedSelfCost); + writeBuff.push_back(function.summedInclusiveCost); + writeBuff.push_back(function.invocationCount); + writeBuff.push_back(function.calledFromInformation.size()); + writeBuff.push_back(function.subCallInformation.size()); + writeBuffer(out, writeBuff); + // Write called from information + for (std::vector<CallData>::const_iterator cfiItr = function.calledFromInformation.begin(); + cfiItr != function.calledFromInformation.end(); ++cfiItr) { + const CallData& call = *cfiItr; + writeBuff.push_back(call.functionNr); + writeBuff.push_back(call.line); + writeBuff.push_back(call.callCount); + writeBuff.push_back(call.summedCallCost); + writeBuffer(out, writeBuff); + } + // Write sub call information + for (std::vector<CallData>::const_iterator sciItr = function.subCallInformation.begin(); + sciItr != function.subCallInformation.end(); ++sciItr) { + const CallData& call = *sciItr; + writeBuff.push_back(call.functionNr); + writeBuff.push_back(call.line); + writeBuff.push_back(call.callCount); + writeBuff.push_back(call.summedCallCost); + writeBuffer(out, writeBuff); + } + + out << function.filename << '\n' << reFunctionNames[index] << '\n'; + } + size_t headersPos = out.tellp(); + // Write headers + for (std::vector<std::string>::const_iterator hItr = headers.begin(); + hItr != headers.end(); ++hItr) { + out << *hItr << '\n'; + } + + // Write addresses + out.seekp(NR_SIZE, std::ios::beg); + writeBuff.push_back(headersPos); + writeBuffer(out, writeBuff); + // Skip function count + out.seekp(NR_SIZE, std::ios::cur); + // Write function addresses + writeBuffer(out, functionAddresses); + + out.close(); + } + +private: + + void getCompressedName(std::string& name, bool isFile) + { + if (name[0] != '(' || !std::isdigit(name[1])) { + return; + } + int functionIndex = std::atoi(name.c_str() + 1); + size_t idx = name.find(')'); + if (idx + 2 < name.length()) { + name.erase(0, idx + 2); + compressedNames[isFile][functionIndex] = name; + } else { + std::map<int, std::string>::iterator nmIt = compressedNames[isFile].find(functionIndex); + if (nmIt != compressedNames[isFile].end()) { + name = nmIt->second; // should always exist for valid files + } + } + } + + void writeBuffer(std::ostream& out, std::vector<uint32_t>& buffer) + { + for (std::vector<uint32_t>::iterator bItr = buffer.begin(); bItr != buffer.end(); ++bItr) { + *bItr = toLittleEndian32(*bItr); + } + out.write(reinterpret_cast<const char*>(&buffer.front()), sizeof(uint32_t) * buffer.size()); + buffer.clear(); + } + + uint32_t toLittleEndian32(uint32_t value) + { + value = htonl(value); + uint32_t result = 0; + result |= (value & 0x000000FF) << 24; + result |= (value & 0x0000FF00) << 8; + result |= (value & 0x00FF0000) >> 8; + result |= (value & 0xFF000000) >> 24; + return result; + } + + std::map<int, std::string> compressedNames [2]; +}; + +int main(int argc, char* argv[]) +{ + if (argc < 3) { + return 1; + } + std::vector<std::string> proxyFunctions; + for (int argIdx = 3; argIdx < argc; ++ argIdx) { + proxyFunctions.push_back(argv[argIdx]); + } + std::sort(proxyFunctions.begin(), proxyFunctions.end()); + Webgrind_Preprocessor processor; + processor.parse(argv[1], argv[2], proxyFunctions); + return 0; +} diff --git a/makefile b/makefile new file mode 100644 index 0000000..bbbcf23 --- /dev/null +++ b/makefile @@ -0,0 +1,20 @@ +CXX = g++ +SRCS = library/preprocessor.cpp +OUT = bin/preprocessor + + +all: $(OUT) + +help: + @echo "Targets:" + @echo " all - build preprocessor" + @echo " clean - clear generated binaries" + @echo " help - show this message\n" + +clean: + rm -f $(OUT) + +$(OUT): $(SRCS) + $(CXX) -o $(OUT) -O2 -s $(SRCS) + +.PHONY: all help clean diff --git a/styles/style.css b/styles/style.css index bd6446b..8548419 100644 --- a/styles/style.css +++ b/styles/style.css @@ -1,21 +1,21 @@ body { - font-family : Helvetica, Verdana, Arial, sans-serif; - font-size : 12px; - color : #000000; - margin:0px; + font-family: Helvetica, Verdana, Arial, sans-serif; + font-size: 12px; + color: #000000; + margin: 0px; } a { - color:#000000; - text-decoration:none; + color: #000000; + text-decoration: none; } a:hover { - text-decoration:underline; + text-decoration: underline; } #footer a { - text-decoration:underline; + text-decoration: underline; } img { @@ -23,41 +23,41 @@ img { } h2 { - font-size:16px; - margin:0px 0 5px 0; - font-weight:normal; + font-size: 16px; + margin: 0px 0 5px 0; + font-weight: normal; } #head { - padding:5px 10px 10px 10px; - border-bottom:1px solid #404040; - background:url(../img/head.png) repeat-x; + padding: 5px 10px 10px 10px; + border-bottom: 1px solid #404040; + background: url(../img/head.png) repeat-x; } #logo { - float:left; + float: left; } #logo h1 { - font:normal 30px "Futura", Helvetica, Verdana; - padding:0px; - margin:0px; + font: normal 30px "Futura", Helvetica, Verdana; + padding: 0px; + margin: 0px; } #logo p { - font:normal 11px "Verdana"; - margin:0px; - padding:0px; + font: normal 11px "Verdana"; + margin: 0px; + padding: 0px; } #options { - float:right; - width:580px; - padding:10px 0 0 0; + float: right; + width: 600px; + padding: 10px 0 0 0; } #options form { - margin:0px; + margin: 0px; } #main { - margin:10px; + margin: 10px; } #hello_message { @@ -66,10 +66,10 @@ h2 { } #trace_view { - display:none; + display: none; } -#runtime_sum, +#runtime_sum, #invocation_sum, #shown_sum { font-weight: bold; @@ -82,7 +82,7 @@ h2 { div.hr { border-top: 1px solid black; - margin:10px 5px; + margin: 10px 5px; } div.callinfo_area { @@ -94,9 +94,9 @@ table.tablesorter { border-width: 1px 0 1px 1px; border-style: solid; border-color: #D9D9D9; - font-family:arial; - margin:10px 0pt 15px; - padding:0px; + font-family: arial; + margin: 10px 0pt 15px; + padding: 0px; font-size: 8pt; width: 100%; text-align: left; @@ -116,15 +116,15 @@ table.tablesorter thead tr .header { } table.tablesorter tbody td { color: #000000; - border-right:1px solid #D9D9D9; + border-right: 1px solid #D9D9D9; padding: 4px; vertical-align: top; } table.tablesorter tbody tr.odd { - background-color:#EAEEF2; + background-color: #EAEEF2; } table.tablesorter tbody tr.even { - background-color:#FFFFFF; + background-color: #FFFFFF; } table.tablesorter thead tr .headerSortUp { @@ -147,29 +147,38 @@ th span{ } img.list_reload { - margin:0px 5px; + margin: 0px 5px; } .block_box { margin: 10px 10px; } -.num { - display:block; - clear:left; - color: gray; - text-align: right; - margin-right: 6pt; - padding-right: 6pt; +.num { + display: block; + clear: left; + color: gray; + text-align: right; + margin-right: 6pt; + padding-right: 6pt; border-right: 1px solid gray; } -.line_emph { - background-color: #bbbbff; +.line { + display: block; +} + +.line:hover { + background-color: #ffd; +} + +.line.emph { + background-color: #dff; } + a.load_invocations { display: none; background-color: #999; border: 1px solid #333; padding: 2px; -}
\ No newline at end of file +} diff --git a/templates/fileviewer.phtml b/templates/fileviewer.phtml index c3f01b8..b1df0eb 100644 --- a/templates/fileviewer.phtml +++ b/templates/fileviewer.phtml @@ -1,51 +1,91 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" - "http://www.w3.org/TR/html4/loose.dtd"> +<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-type" content="text/html; charset=utf-8"> <script src="js/jquery.js" type="text/javascript" charset="utf-8"></script> <script src="js/jquery.scrollTo.js" type="text/javascript" charset="utf-8"></script> <link rel="stylesheet" type="text/css" href="styles/style.css"> + <link rel="shortcut icon" type="image/ico" href="favicon.ico"> <title> webgrind - fileviewer: <?php echo $file?> </title> <script type="text/javascript" charset="utf-8"> - $(document).ready(function() { - $('div#'+location.hash.substr(1)).addClass('line_emph'); - }); + $(document).ready(function() { + $('#'+location.hash.substr(1)).addClass('emph'); + if (typeof window.addEventListener == "function") { + window.addEventListener("hashchange", function(e) { + $("code").removeClass('emph'); + if (window.location.hash.length > 2) + $('#'+location.hash.substr(1)).addClass('emph'); + }); + } + }); </script> - + </head> <body> - <div id="head"> - <div id="logo"> - <h1>webgrind<sup style="font-size:10px">v<?php echo Webgrind_Config::$webgrindVersion?></sup></h1> - <p>profiling in the browser</p> - </div> - <div style="clear:both;"></div> - </div> - <div id="main"> - <h2><?php echo $file?></h2> - <br> - <?php if ($message==''):?> - <?php $source = highlight_file($file, true); ?> - <table border="0"> - <tr> - <td align="right" valign="top"><code> - <?php - foreach ($lines = explode('<br />', $source) as $num => $line) { - $num++; - echo "<span class='num' name='line$num' id='line$num'>$num</span>"; - } - ?> - </code></td> - <td valign="top" nowrap="nowrap"><?php echo $source; ?></td> - </tr> - </table> - <?php else:?> - <p><b><?php echo $message?></b></p> - <?php endif?> - </div> + <div id="head"> + <div id="logo"> + <h1>webgrind<sup style="font-size:10px">v<?php echo Webgrind_Config::$webgrindVersion?></sup></h1> + <p>profiling in the browser</p> + </div> + <div style="clear:both;"></div> + </div> + <div id="main"> + <h2><?php echo $file?></h2> + <br> + <?php if ($message==''): ?> + <table border="0"> + <tr> + <td align="right" valign="top"><code> + <?php + // Strip code and first span + $hl = highlight_file($file, true); + $code = substr($hl, 36, -15); + // Wrap missing spans + $code = preg_replace( + array('#([^>])<span#', '#</span>([^<])#'), + array('\1</span><span', '</span><span>\1'), + $code + ); + if ($code[0] != '<') { + $code = '<span>'.$code; + } + // Split lines + $lines = explode('<br />', $code); + + foreach ($lines as $num => $line) { + $num++; + echo "<a href='#line$num'><span class='num'>$num</span></a>"; + } + ?> + </code></td> + <td valign="top" nowrap="nowrap"> + <?php + $openSpan = ''; + foreach ($lines as $num => $line) { + $num++; + if (!$line) { + $line = '<br />'; + } + if (!preg_match('#</span>\s*$#', $line)) { + $line .= '</span>'; + } + + echo "<code id='line$num' class='line'>$openSpan$line</code>"; + + if (preg_match('#.*(<span[^>]*>)#', $line, $matches)) { + $openSpan = $matches[1]; + } + } + ?> + </td> + </tr> + </table> + <?php else:?> + <p><b><?php echo $message?></b></p> + <?php endif?> + </div> </body> -</html>
\ No newline at end of file +</html> diff --git a/templates/index.phtml b/templates/index.phtml index 7c3771a..5dc0a84 100644 --- a/templates/index.phtml +++ b/templates/index.phtml @@ -1,10 +1,10 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" - "http://www.w3.org/TR/html4/loose.dtd"> +<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-type" content="text/html; charset=utf-8"> <title>webgrind</title> <link rel="stylesheet" type="text/css" href="styles/style.css"> + <link rel="shortcut icon" type="image/ico" href="favicon.ico"> <script src="js/jquery.js" type="text/javascript" charset="utf-8"></script> <script src="js/jquery.blockUI.js" type="text/javascript" charset="utf-8"></script> <script src="js/jquery.tablesorter.js" type="text/javascript" charset="utf-8"></script> @@ -15,7 +15,7 @@ var currentDataFile = null; var callInfoLoaded = new Array(); var disableAjaxBlock = false; - function getOptions(specificFile){ + function getOptions(specificFile) { var options = new Object(); options.dataFile = specificFile || $("#dataFile").val(); options.costFormat = $('#costFormat').val(); @@ -23,21 +23,21 @@ options.hideInternals = $('#hideInternals').attr('checked') ? 1 : 0; return options; } - - function update(specificFile){ + + function update(specificFile) { vars = getOptions(specificFile); vars.op = 'function_list'; $.getJSON("index.php", vars, - function(data){ - if (data.error) { - $("#hello_message").html(data.error); - $("#hello_message").show(); - return; - } + function(data) { + if (data.error) { + $("#hello_message").html(data.error); + $("#hello_message").show(); + return; + } callInfoLoaded = new Array(); $("#function_table tbody").empty(); - for(i=0;i<data.functions.length;i++){ + for (i=0; i<data.functions.length; i++) { callInfoLoaded[data.functions[i].nr] = false; $("#function_table tbody").append(functionTableRow(data.functions[i], data.linkToFunctionLine)); } @@ -50,25 +50,35 @@ $("#invocation_sum").html(data.summedInvocationCount); $("#runtime_sum").html(data.summedRunTime); $("#runs").html(data.runs); - + var breakdown_sum = data.breakdown['internal']+data.breakdown['procedural']+data.breakdown['class']+data.breakdown['include']; - $("#breakdown").html( - '<img src="img/gradient_left.png" height="20" width="10">'+ - '<img src="img/gradient_internal.png" height="20" width="'+Math.floor(data.breakdown['internal']/breakdown_sum*300)+'">'+ - '<img src="img/gradient_include.png" height="20" width="'+Math.floor(data.breakdown['include']/breakdown_sum*300)+'">'+ - '<img src="img/gradient_class.png" height="20" width="'+Math.floor(data.breakdown['class']/breakdown_sum*300)+'">'+ - '<img src="img/gradient_procedural.png" height="20" width="'+Math.floor(data.breakdown['procedural']/breakdown_sum*300)+'">'+ - '<img src="img/gradient_right.png" height="20" width="10">'+ - '<div title="internal functions, include/require, class methods and procedural functions." style="position:relative;top:-20px;left:10px;width:301px;height:19px"></div>' - ); - + $("#breakdown").html( + '<img src="img/gradient_left.png" height="20" width="10">'+ + '<img src="img/gradient_internal.png" title="internal functions" height="20" width="'+Math.floor(data.breakdown['internal']/breakdown_sum*300)+'">'+ + '<img src="img/gradient_include.png" title="include/require" height="20" width="'+Math.floor(data.breakdown['include']/breakdown_sum*300)+'">'+ + '<img src="img/gradient_class.png" title="class methods" height="20" width="'+Math.floor(data.breakdown['class']/breakdown_sum*300)+'">'+ + '<img src="img/gradient_procedural.png" title="procedural functions" height="20" width="'+Math.floor(data.breakdown['procedural']/breakdown_sum*300)+'">'+ + '<img src="img/gradient_right.png" height="20" width="10">' + ); + $("#hello_message").hide(); $("#trace_view").show(); - + $("#function_table").trigger('update'); - $("#function_table").trigger("sorton",[[[4,1]]]); - + <?php + $sortCol = 0; + foreach (Webgrind_Config::$tableFields as $idx => $field) { + if (strpos($field, 'Cost') !== FALSE) { + $sortCol = 3 + $idx; + break; + } + } + ?> + $("#function_table").trigger("sorton",[[[<?php echo $sortCol?>,1]]]); + $('#callfilter').trigger('keyup'); + if (window.location.hash.length > 2) + openCallInfo(window.location.hash.replace(/[^0-9]/g, '')); } ); } @@ -80,13 +90,13 @@ delete vars.hideInternals; window.open('index.php?' + $.param(vars)); } - - function reloadFilelist(){ + + function reloadFilelist() { $.getJSON("index.php", {'op':'file_list'}, function(data){ var options = new Object(); - for(i=0;i<data.length;i++){ + for (i=0; i<data.length; i++) { options[data[i]['filename']] = data[i]['invokeUrl']+' ('+data[i]['filename']+')'+' ['+data[i]['filesize']+']'; } $("#dataFile").removeOption(/[^0]/); @@ -95,55 +105,52 @@ } ); } - - function loadCallInfo(functionNr){ + + function loadCallInfo(functionNr) { $.getJSON("index.php", {'op':'callinfo_list', 'file':currentDataFile, 'functionNr':functionNr, 'costFormat':$("#costFormat").val()}, - function(data){ + function(data) { if (data.error) { - $("#hello_message").html(data.error); - $("#hello_message").show(); - return; - } - - if(data.calledByHost) + $("#hello_message").html(data.error); + $("#hello_message").show(); + return; + } + + if (data.calledByHost) $("#callinfo_area_"+functionNr).append('<b>Called from script host</b>'); - + insertCallInfo(functionNr, 'sub_calls_table_', 'Calls', data.subCalls); insertCallInfo(functionNr, 'called_from_table_', 'Called From', data.calledFrom); - callInfoLoaded[functionNr] = true; + window.location.hash = "#callinfo_a_"+functionNr; } - ); - + ); } - - function insertCallInfo(functionNr, idPrefix, title, data){ - if(data.length==0) + + function insertCallInfo(functionNr, idPrefix, title, data) { + if (data.length==0) return; - + $("#callinfo_area_"+functionNr).append(callTable(functionNr,idPrefix, title)); - + for(i=0;i<data.length;i++){ $("#"+idPrefix+functionNr+" tbody").append(callTableRow(i, data[i])); } - + $("#"+idPrefix+functionNr).tablesorter({ widgets: ['zebra'], - headers: { - 3: { - sorter: false - } - } + headers: { + 3: { + sorter: false + } + } }); $("#"+idPrefix+functionNr).bind("sortStart",sortBlock).bind("sortEnd",$.unblockUI); - $("#"+idPrefix+functionNr).trigger("sorton",[[[2,1]]]); - - + $("#"+idPrefix+functionNr).trigger("sorton",[[[2,1]]]); } - - function callTable(functionNr, idPrefix, title){ + + function callTable(functionNr, idPrefix, title) { return '<table class="tablesorter" id="'+idPrefix+functionNr+'" cellspacing="0"> \ <thead><tr><th><span>'+title+'</span></th><th><span>Count</span></th><th><span>Total Call Cost</span></th><th> </th></tr></thead> \ <tbody> \ @@ -151,8 +158,8 @@ </table> \ '; } - - function callTableRow(nr,data){ + + function callTableRow(nr, data) { return '<tr> \ <td>' +($("#callinfo_area_"+data.functionNr).length ? '<img src="img/right.gif"> <a href="javascript:openCallInfo('+data.functionNr+')">'+data.callerFunctionName+'</a>' : '<img src="img/blank.gif"> '+data.callerFunctionName) @@ -161,212 +168,259 @@ <td class="nr">'+data.summedCallCost+'</td> \ <td><a title="Open file and show line" href="'+sprintf(fileUrlFormat,data.file,data.line)+'" target="_blank"><img src="img/file_line.png" alt="O"></a></td> \ </tr>'; - } - - function toggleCallInfo(functionNr){ - if(!callInfoLoaded[functionNr]){ - loadCallInfo(functionNr); - } - - $("#callinfo_area_"+functionNr).toggle(); - current = $("#fold_marker_"+functionNr).get(0).src; - if(current.substr(current.lastIndexOf('/')+1) == 'right.gif') - $("#fold_marker_"+functionNr).get(0).src = 'img/down.gif'; - else - $("#fold_marker_"+functionNr).get(0).src = 'img/right.gif'; + + function toggleCallInfo(functionNr) { + var $ciar = $("#callinfo_area_"+functionNr); + var fmimg = $("#fold_marker_"+functionNr).get(0); + var current = $("#fold_marker_"+functionNr).get(0).src; + if ($ciar.is(":visible")) { + $ciar.hide(); + fmimg.src = 'img/right.gif'; + } else { + if (!callInfoLoaded[functionNr]) { + loadCallInfo(functionNr); + } else { + window.location.hash = "#callinfo_a_"+functionNr; + } + $ciar.show(); + fmimg.src = 'img/down.gif'; + } } - + function openCallInfo(functionNr) { var areaEl = $("#callinfo_area_"+functionNr); if (areaEl.length) { - if (areaEl.is(":hidden")) toggleCallInfo(functionNr); - window.scrollTo(0, areaEl.parent().offset().top); - } + if (areaEl.is(":hidden")) { + toggleCallInfo(functionNr); + } else { + window.location.hash = "#callinfo_a_"+functionNr; + } + window.scrollTo(0, areaEl.parent().offset().top); + setTimeout(function(){areaEl.parent().parent().css({'background-color' : '#DFECE0'})}, 50); + setTimeout(function(){areaEl.parent().parent().css({'background-color' : ''})}, 600); + } } - - function functionTableRow(data, linkToFunctionLine){ + + function functionTableRow(data, linkToFunctionLine) { if (data.file=='php%3Ainternal') { - openLink = '<a title="Lookup function" href="<?php echo ini_get('xdebug.manual_url')?>/'+data.functionName.substr(5)+'" target="_blank"><img src="img/file.png" alt="O"></a>';; + openLink = '<a title="Lookup function" href="http://php.net/'+data.functionName.substr(5).replace("->",".")+'" target="_blank"><img src="img/file.png" alt="O"></a>'; } else { - if(linkToFunctionLine){ - openLink = '<a title="Open file and show line" href="'+sprintf(fileUrlFormat, data.file, data.line)+'" target="_blank"><img src="img/file_line.png" alt="O"></a>'; - } else { - openLink = '<a title="Open file" href="'+sprintf(fileUrlFormat, data.file, -1)+'" target="_blank"><img src="img/file.png" alt="O"></a>'; - } + if (linkToFunctionLine) { + openLink = '<a title="Open file and show line" href="'+sprintf(fileUrlFormat, data.file, data.line)+'" target="_blank"><img src="img/file_line.png" alt="O"></a>'; + } else { + openLink = '<a title="Open file" href="'+sprintf(fileUrlFormat, data.file, -1)+'" target="_blank"><img src="img/file.png" alt="O"></a>'; + } } return '<tr> \ - <td> \ - <img src="img/call_'+data.humanKind+'.png" title="'+data.humanKind+'"> \ - </td> \ <td> \ - <a href="javascript:toggleCallInfo('+data.nr+')"> \ + <img src="img/call_'+data.humanKind+'.png" title="'+data.humanKind+'"> \ + </td> \ + <td> \ + <a id="callinfo_a_'+data.nr+'" href="javascript:toggleCallInfo('+data.nr+')"> \ <img id="fold_marker_'+data.nr+'" src="img/right.gif"> '+data.functionName+' \ </a> \ <div class="callinfo_area" id="callinfo_area_'+data.nr+'"></div> \ </td> \ <td>'+openLink+'</td> \ - <td class="nr">'+data.invocationCount+'</td> \ - <td class="nr">'+data.summedSelfCost+'</td> \ - <td class="nr">'+data.summedInclusiveCost+'</td> \ + <?php + $dataCodes = array( + 'Invocation Count' => 'data.invocationCount', + 'Total Self Cost' => 'data.summedSelfCost', + 'Average Self Cost' => 'sprintf("%.2f", data.summedSelfCost/data.invocationCount)', + 'Total Inclusive Cost' => 'data.summedInclusiveCost', + 'Average Inclusive Cost' => 'sprintf("%.2f", data.summedInclusiveCost/data.invocationCount)', + ); + foreach (Webgrind_Config::$tableFields as $field) { + echo "<td class=\"nr\">'+$dataCodes[$field]+'</td> \\\n\t\t\t\t\t\t"; + } + ?> </tr> \ '; } - - function sortBlock(){ + + function sortBlock() { $.blockUI('<div class="block_box"><h1>Sorting...</h1></div>'); } - - function loadBlock(){ - if(!disableAjaxBlock) + + function loadBlock() { + if (!disableAjaxBlock) $.blockUI(); disableAjaxBlock = false; } - - function checkVersion(){ + + function checkVersion() { disableAjaxBlock = true; $.getJSON("index.php", {'op':'version_info'}, - function(data){ - if(data.latest_version><?php echo Webgrind_Config::$webgrindVersion?>){ + function(data) { + if (data.latest_version><?php echo Webgrind_Config::$webgrindVersion?>) { $("#version_info").append('Version '+data.latest_version+' is available for <a href="'+data.download_url+'">download</a>.'); } else { $("#version_info").append('You have the latest version.'); } } - ); - + ); + } + + function clearFiles() { + $.getJSON("index.php", + {'op':'clear_files'}, + function(data) { + if (data.error) { + $("#hello_message").html(data.error); + $("#hello_message").show(); + return; + } + reloadFilelist(); + } + ); } - - $(document).ready(function() { - + + + $(document).ready(function() { $.blockUI.defaults.pageMessage = '<div class="block_box"><h1>Loading...</h1><p>Loading information from server. If the callgrind file is large this may take some time.</p></div>'; - $.blockUI.defaults.overlayCSS = { backgroundColor: '#fff', opacity: '0' }; + $.blockUI.defaults.overlayCSS = { backgroundColor: '#fff', opacity: '0' }; $.blockUI.defaults.fadeIn = 0; $.blockUI.defaults.fadeOut = 0; $().ajaxStart(loadBlock).ajaxStop($.unblockUI); $("#function_table").tablesorter({ widgets: ['zebra'], sortInitialOrder: 'desc', - headers: { - 1: { - sorter: false - }, - 2: { - sorter: false - } - } + headers: { + 1: { + sorter: false + }, + 2: { + sorter: false + } + } }); $("#function_table").bind("sortStart",sortBlock).bind("sortEnd",$.unblockUI); - - if(document.location.hash) { - update(document.location.hash.substr(1)); + + if (document.location.hash) { + update(); } - + <?php if(Webgrind_Config::$checkVersion):?> setTimeout(checkVersion,100); <?php endif?> - + + $("#negateFilter").change(function(){ + $('#callfilter').trigger('keyup'); + }); + $("#callfilter").keyup(function(){ - var reg = new RegExp($(this).val(), 'i'); - var row; - $('#function_table').children('tbody').children('tr').each(function(){ - row = $(this); - if (row.find('td:eq(1) a').text().match(reg)) - row.css('display', 'table-row'); - else - row.css('display', 'none'); - }); - }); + var reg = new RegExp($(this).val(), 'i'); + var negate = $("#negateFilter").is(':checked'); + var row; + $('#function_table').children('tbody').children('tr').each(function(){ + row = $(this); + if (!row.find('td:eq(1) a').text().match(reg) == negate) + row.css('display', 'table-row'); + else + row.css('display', 'none'); + }); + }); + + if (typeof window.addEventListener == "function") { + window.addEventListener("hashchange", function(e) { + if (window.location.hash.length > 2) + openCallInfo(window.location.hash.replace(/[^0-9]/g, '')); + }); + } }); - </script> </head> <body> - <div id="head"> - <div id="logo"> - <h1>webgrind<sup style="font-size:10px">v<?php echo Webgrind_Config::$webgrindVersion?></sup></h1> - <p>profiling in the browser</p> - </div> - <div id="options"> - <form method="get" onsubmit="update();return false;"> - <div style="float:right;margin-left:10px"> - <input type="submit" value="update"> - </div> - <div style="float:right;"> - <label style="margin:0 5px">in</label> - <select id="costFormat" name="costFormat"> - <option value="percent" <?php echo (Webgrind_Config::$defaultCostformat=='percent') ? 'selected' : ''?>>percent</option> - <option value="msec" <?php echo (Webgrind_Config::$defaultCostformat=='msec') ? 'selected' : ''?>>milliseconds</option> - <option value="usec" <?php echo (Webgrind_Config::$defaultCostformat=='usec') ? 'selected' : ''?>>microseconds</option> - </select> - </div> - <div style="float:right;"> - <label style="margin:0 5px">of</label> - <select id="dataFile" name="dataFile" style="width:200px"> - <option value="0">Auto (newest)</option> - <?php foreach(Webgrind_FileHandler::getInstance()->getTraceList() as $trace):?> - <!-- <option value="<?php echo $trace['filename']?>"><?php echo $trace['invokeUrl']?> (<?php echo $trace['filename']?>) [<?php echo $trace['filesize']?>]</option> --> - <option value="<?php echo $trace['filename']?>"><?php echo str_replace(array('%i','%f','%s','%m'),array($trace['invokeUrl'],$trace['filename'],$trace['filesize'],$trace['mtime']),Webgrind_Config::$traceFileListFormat); ?></option> - <?php endforeach;?> - </select> - <img class="list_reload" src="img/reload.png" onclick="reloadFilelist()"> - </div> - <div style="float:right"> - <label style="margin:0 5px">Show</label> - <select id="showFraction" name="showFraction"> - <?php for($i=100; $i>0; $i-=10):?> - <option value="<?php echo $i/100?>" <?php if ($i==Webgrind_Config::$defaultFunctionPercentage):?>selected="selected"<?php endif;?>><?php echo $i?>%</option> - <?php endfor;?> - </select> - </div> - <div style="clear:both;"></div> - <div style="margin:0 70px"> - <input type="checkbox" name="hideInternals" value="1" <?php echo (Webgrind_Config::$defaultHideInternalFunctions==1) ? 'checked' : ''?> id="hideInternals"> - <label for="hideInternals">Hide PHP functions</label> - </div> - </form> - </div> - - <div style="clear:both;"></div> - </div> - <div id="main"> - - <div id="trace_view"> - <div style="float:left;"> - <h2 id="invoke_url"></h2> - <span id="data_file"></span> @ <span id="mtime"></span> - </div> - <div style="float:right;"> - <div id="breakdown" style="margin-bottom:5px;width:320px;height:20px"></div> - <span id="invocation_sum"></span> different functions called in <span id="runtime_sum"></span> milliseconds (<span id="runs"></span> runs, <span id="shown_sum"></span> shown) + <div id="head"> + <div id="logo"> + <h1>webgrind<sup style="font-size:10px">v<?php echo Webgrind_Config::$webgrindVersion?></sup></h1> + <p>profiling in the browser</p> + </div> + <div id="options"> + <form method="get" onsubmit="window.location.hash='';update();return false;"> + <div style="float:right;margin-left:10px"> + <input type="submit" value="update"> + </div> + <div style="float:right;"> + <label style="margin:0 5px">in</label> + <select id="costFormat" name="costFormat"> + <option value="percent" <?php echo (Webgrind_Config::$defaultCostformat=='percent') ? 'selected' : ''?>>percent</option> + <option value="msec" <?php echo (Webgrind_Config::$defaultCostformat=='msec') ? 'selected' : ''?>>milliseconds</option> + <option value="usec" <?php echo (Webgrind_Config::$defaultCostformat=='usec') ? 'selected' : ''?>>microseconds</option> + </select> + </div> + <div style="float:right;"> + <label style="margin:0 5px">of</label> + <select id="dataFile" name="dataFile" style="width:200px"> + <option value="0">Auto (newest)</option> + <?php foreach(Webgrind_FileHandler::getInstance()->getTraceList() as $trace):?> + <!-- <option value="<?php echo $trace['filename']?>"><?php echo $trace['invokeUrl']?> (<?php echo $trace['filename']?>) [<?php echo $trace['filesize']?>]</option> --> + <option value="<?php echo $trace['filename']?>"><?php echo str_replace(array('%i','%f','%s','%m'),array($trace['invokeUrl'],$trace['filename'],$trace['filesize'],$trace['mtime']),Webgrind_Config::$traceFileListFormat); ?></option> + <?php endforeach;?> + </select> + <img class="list_reload" src="img/reload.png" onclick="reloadFilelist()"> + </div> + <div style="float:right"> + <label style="margin:0 5px">Show</label> + <select id="showFraction" name="showFraction"> + <?php foreach(array(100, 99.7, 98, 95, 90, 82, 68, 50, 26) as $i):?> + <option value="<?php echo $i/100?>" <?php if ($i==Webgrind_Config::$defaultFunctionPercentage):?>selected="selected"<?php endif;?>><?php echo $i?>%</option> + <?php endforeach;?> + </select> + </div> + <div style="clear:both;"></div> + <div style="margin:0 70px"> + <input type="checkbox" name="hideInternals" value="1" <?php echo (Webgrind_Config::$defaultHideInternalFunctions==1) ? 'checked' : ''?> id="hideInternals"> + <label for="hideInternals">Hide PHP functions</label> + <input type="button" value="clear files" onclick="clearFiles()"> + </div> + </form> + </div> + + <div style="clear:both;"></div> + </div> + <div id="main"> + + <div id="trace_view"> + <div style="float:left;"> + <h2 id="invoke_url"></h2> + <span id="data_file"></span> @ <span id="mtime"></span> + </div> + <div style="float:right;"> + <div id="breakdown" style="margin-bottom:5px;width:320px;height:20px"></div> + <span id="invocation_sum"></span> different functions called in <span id="runtime_sum"></span> milliseconds (<span id="runs"></span> runs, <span id="shown_sum"></span> shown) <div><input type="button" name="graph" value="Show Call Graph" onclick="showCallGraph()"></div> - </div> - <div style="clear:both"></div> - Filter: <input type="text" style="width:150px" id="callfilter"> (regex too) - <table class="tablesorter" id="function_table" cellspacing="0"> - <thead> - <tr> - <th> </th> - <th><span>Function</span></th> - <th> </th> - <th><span>Invocation Count</span></th> - <th><span>Total Self Cost</span></th> - <th><span>Total Inclusive Cost</span></th> - </tr> - </thead> - <tbody> - </tbody> - </table> - </div> - <h2 id="hello_message"><?php echo $welcome?></h2> - <div id="footer"> - <?php if(Webgrind_Config::$checkVersion):?> - <div id="version_info"> </div> - <?php endif?> - Copyright © 2008-2011 Jacob Oettinger & Joakim Nygård. <a href="http://github.com/jokkedk/webgrind/">webgrind homepage</a> - </div> - </div> + </div> + <div style="clear:both"></div> + Filter: <input type="text" style="width:150px" id="callfilter"> (regex too) + <input type="checkbox" name="negateFilter" value="0" id="negateFilter"> + <label for="negateFilter">Invert filter</label> + <table class="tablesorter" id="function_table" cellspacing="0"> + <thead> + <tr> + <th> </th> + <th><span>Function</span></th> + <th> </th> + <?php foreach(Webgrind_Config::$tableFields as $field):?> + <th><span><?php echo $field?></span></th> + <?php endforeach;?> + </tr> + </thead> + <tbody> + </tbody> + </table> + </div> + <h2 id="hello_message"><?php echo $welcome?></h2> + <div id="footer"> + <?php if(Webgrind_Config::$checkVersion):?> + <div id="version_info"> </div> + <?php endif?> + Copyright © 2008-2011 Jacob Oettinger & Joakim Nygård. <a href="http://github.com/jokkedk/webgrind/">webgrind homepage</a> + <a href="http://github.com/alpha0010/webgrind/">dev fork</a> + </div> + </div> </body> -</html>
\ No newline at end of file +</html> |