summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlpha <ngcoder@live.com>2015-07-23 09:19:48 -0400
committerAlpha <ngcoder@live.com>2015-07-23 09:19:48 -0400
commitbeabc7aa344be2b6dd809661098f891497800f84 (patch)
treea9bdf3f45f353e98e69a2e94ffe6d507a05dcb0f
parent077f79f13ddc8efa4cb10c2c041eb4b50037e0e3 (diff)
downloadwebgrind-beabc7aa344be2b6dd809661098f891497800f84.zip
webgrind-beabc7aa344be2b6dd809661098f891497800f84.tar.gz
webgrind-beabc7aa344be2b6dd809661098f891497800f84.tar.bz2
Harmonize whitespace
Strip trailing whitespace and attempt to have all files stay unified with their coding style.
-rw-r--r--config.php160
-rw-r--r--index.php37
-rw-r--r--library/FileHandler.php376
-rw-r--r--library/Preprocessor.php277
-rw-r--r--library/Reader.php492
-rw-r--r--library/gprof2dot.py210
-rw-r--r--styles/style.css88
-rw-r--r--templates/fileviewer.phtml65
-rw-r--r--templates/index.phtml392
9 files changed, 1042 insertions, 1055 deletions
diff --git a/config.php b/config.php
index b3c905a..bf3234c 100644
--- a/config.php
+++ b/config.php
@@ -6,58 +6,58 @@
* @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;
+ /**
+ * 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';
- /**
- * 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'; // ?
+ 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;
/**
- * 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)
- */
+ * 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'; // ?
+
+ /**
+ * 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]';
@@ -65,39 +65,39 @@ class Webgrind_Config extends Webgrind_MasterConfig {
# 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()).'/';
+ }
}
diff --git a/index.php b/index.php
index 399b7ad..4826904 100644
--- a/index.php
+++ b/index.php
@@ -13,7 +13,7 @@ require './config.php';
require './library/FileHandler.php';
// TODO: Errorhandling:
-// No files, outputdir not writable
+// No files, outputdir not writable
set_time_limit(0);
@@ -22,10 +22,11 @@ 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());
break;
+
case 'function_list':
$dataFile = get('dataFile');
if($dataFile=='0'){
@@ -37,7 +38,7 @@ 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);
@@ -90,9 +91,10 @@ try {
$creator = preg_replace('/[^0-9\.]/', '', $reader->getHeader('creator'));
$result['linkToFunctionLine'] = version_compare($creator, '2.1') > 0;
-
+
echo json_encode($result);
break;
+
case 'callinfo_list':
$reader = Webgrind_FileHandler::getInstance()->getTraceReader(get('file'), get('costFormat', Webgrind_Config::$defaultCostformat));
$functionNr = get('functionNr');
@@ -110,7 +112,7 @@ 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']
@@ -119,6 +121,7 @@ try {
}
echo json_encode($result);
break;
+
case 'fileviewer':
$file = get('file');
$line = get('line');
@@ -136,8 +139,8 @@ try {
$message = 'No file to view';
}
require 'templates/fileviewer.phtml';
-
break;
+
case 'function_graph':
$dataFile = get('dataFile');
$showFraction = 100 - intval(get('showFraction') * 100);
@@ -147,16 +150,18 @@ try {
}
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().''.$dataFile.' | '.Webgrind_Config::$dotExecutable.' -Tpng -o ' . $filename);
- }
- readfile($filename);
- break;
- case 'version_info':
- $response = @file_get_contents('http://jokke.dk/webgrindupdate.json?version='.Webgrind_Config::$webgrindVersion);
- echo $response;
- break;
- default:
+ if (!file_exists($filename)) {
+ shell_exec(Webgrind_Config::$pythonExecutable.' library/gprof2dot.py -n '.$showFraction.' -f callgrind '.Webgrind_Config::xdebugOutputDir().''.$dataFile.' | '.Webgrind_Config::$dotExecutable.' -Tpng -o ' . $filename);
+ }
+ readfile($filename);
+ break;
+
+ case 'version_info':
+ $response = @file_get_contents('http://jokke.dk/webgrindupdate.json?version='.Webgrind_Config::$webgrindVersion);
+ echo $response;
+ 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>';
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..9205a62 100644
--- a/library/Preprocessor.php
+++ b/library/Preprocessor.php
@@ -1,70 +1,68 @@
<?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 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),
+ /**
+ * 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,
@@ -72,90 +70,85 @@ class Webgrind_Preprocessor
'summedInclusiveCost' => 0,
'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));
- }
-
- }
-
+ );
+ }
+ $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;
+ } elseif (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;
+ } elseif (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));
+ }
+ }
+
}
diff --git a/library/Reader.php b/library/Reader.php
index 35c5113..cb306b5 100644
--- a/library/Reader.php
+++ b/library/Reader.php
@@ -1,269 +1,267 @@
<?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['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']))
+ 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 {
+ } 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;
+ }
+
+ /**
+ * 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);
+ }
+
}
diff --git a/library/gprof2dot.py b/library/gprof2dot.py
index dc7bc8d..99a8c1b 100644
--- a/library/gprof2dot.py
+++ b/library/gprof2dot.py
@@ -79,7 +79,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 +111,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
@@ -145,13 +145,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 +162,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 +186,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)))
@@ -270,7 +270,7 @@ class Profile(Object):
sys.stderr.write("Cycle:\n")
for member in cycle.functions:
sys.stderr.write("\tFunction %s\n" % member.name)
-
+
def _tarjan(self, function, order, stack, orders, lowlinks, visited):
"""Tarjan's strongly connected components algorithm.
@@ -349,7 +349,7 @@ class Profile(Object):
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():
@@ -374,7 +374,7 @@ class Profile(Object):
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
@@ -396,7 +396,7 @@ class Profile(Object):
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():
@@ -408,7 +408,7 @@ class Profile(Object):
callees[callee] += call.ratio
except KeyError:
callees[callee] = call.ratio
-
+
for member in cycle.functions:
member[outevent] = outevent.null()
@@ -509,11 +509,11 @@ class Profile(Object):
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
@@ -530,7 +530,7 @@ class Profile(Object):
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():
sys.stderr.write('Function %s:\n' % (function.name,))
@@ -557,7 +557,7 @@ class Struct:
if attrs is None:
attrs = {}
self.__dict__['_attrs'] = attrs
-
+
def __getattr__(self, name):
try:
return self._attrs[name]
@@ -572,7 +572,7 @@ class Struct:
def __repr__(self):
return repr(self._attrs)
-
+
class ParseError(Exception):
"""Raised when parsing to signal mismatches."""
@@ -595,7 +595,7 @@ class Parser:
def parse(self):
raise NotImplementedError
-
+
class LineParser(Parser):
"""Base class for parsers that read line-based formats."""
@@ -664,21 +664,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 +689,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:
@@ -742,13 +742,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 +762,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()
@@ -840,20 +840,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 +862,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 +892,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 +913,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 +923,7 @@ class GprofParser(Parser):
else:
child = self.translate(mo)
children.append(child)
-
+
function.parents = parents
function.children = children
@@ -948,7 +948,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,16 +975,16 @@ 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():
cycles[index] = Cycle()
@@ -999,16 +999,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,7 +1024,7 @@ 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)
@@ -1046,7 +1046,7 @@ class GprofParser(Parser):
class CallgrindParser(LineParser):
"""Parser for valgrind's callgrind tool.
-
+
See also:
- http://valgrind.org/docs/manual/cl-format.html
"""
@@ -1153,7 +1153,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+)*' +
'$')
@@ -1188,12 +1188,12 @@ class CallgrindParser(LineParser):
events = map(float, 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:
@@ -1255,7 +1255,7 @@ class CallgrindParser(LineParser):
def parse_position_spec(self):
line = self.lookahead()
-
+
if line.startswith('jump=') or line.startswith('jcnd='):
self.consume()
return True
@@ -1338,20 +1338,20 @@ 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 OprofileParser(LineParser):
"""Parser for oprofile callgraph output.
-
+
See also:
- http://oprofile.sourceforge.net/doc/opreport.html#opreport-callgraph
"""
@@ -1380,7 +1380,7 @@ 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():
try:
@@ -1389,7 +1389,7 @@ class OprofileParser(LineParser):
totals[partial.id] = partial
else:
total.samples += partial.samples
-
+
def parse(self):
# read lookahead
self.readline()
@@ -1401,7 +1401,7 @@ class OprofileParser(LineParser):
profile = Profile()
reverse_call_samples = {}
-
+
# populate the profile
profile[SAMPLES] = 0
for _callers, _function, _callees in self.entries.itervalues():
@@ -1424,7 +1424,7 @@ class OprofileParser(LineParser):
call = Call(_callee.id)
call[SAMPLES2] = _callee.samples
function.add_call(call)
-
+
# compute derived data
profile.validate()
profile.find_cycles()
@@ -1510,7 +1510,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 +1518,7 @@ class OprofileParser(LineParser):
class HProfParser(LineParser):
"""Parser for java hprof output
-
+
See also:
- http://java.sun.com/developer/technicalArticles/Programming/HPROF.html
"""
@@ -1679,7 +1679,7 @@ class SysprofParser(XmlParser):
def build_profile(self, objects, nodes):
profile = Profile()
-
+
profile[SAMPLES] = 0
for id, object in objects.iteritems():
# Ignore fake objects (process names, modules, "Everything", "kernel", etc.)
@@ -1753,7 +1753,7 @@ class SharkParser(LineParser):
else:
function_total, callees_total = entry
function_total.samples += function.samples
-
+
def add_callee(self, function, callee):
func, callees = self.entries[function.id]
try:
@@ -1762,7 +1762,7 @@ class SharkParser(LineParser):
callees[callee.id] = callee
else:
entry.samples += callee.samples
-
+
def parse(self):
self.readline()
self.readline()
@@ -1800,9 +1800,9 @@ class SharkParser(LineParser):
# 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():
@@ -1818,7 +1818,7 @@ class SharkParser(LineParser):
call = Call(_callee.id)
call[SAMPLES] = _callee.samples
function.add_call(call)
-
+
# compute derived data
profile.validate()
profile.find_cycles()
@@ -1843,7 +1843,7 @@ class XPerfParser(Parser):
def parse(self):
import csv
reader = csv.reader(
- self.stream,
+ self.stream,
delimiter = ',',
quotechar = None,
escapechar = None,
@@ -1856,7 +1856,7 @@ class XPerfParser(Parser):
self.parse_header(row)
for row in it:
self.parse_row(row)
-
+
# compute derived data
self.profile.validate()
self.profile.find_cycles()
@@ -1884,7 +1884,7 @@ class XPerfParser(Parser):
else:
break
fields[name] = value
-
+
process = fields['Process Name']
symbol = fields['Module'] + '!' + fields['Function']
weight = fields['Weight']
@@ -1950,12 +1950,12 @@ 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+)$'
)
@@ -1965,7 +1965,7 @@ class SleepyParser(Parser):
mo = self._symbol_re.match(line)
if mo:
symbol_id, module, procname, sourcefile, sourceline = mo.groups()
-
+
function_id = ':'.join([module, procname])
try:
@@ -1991,7 +1991,7 @@ class SleepyParser(Parser):
callee[SAMPLES] += samples
self.profile[SAMPLES] += samples
-
+
for caller in callstack[1:]:
try:
call = caller.calls[callee.id]
@@ -2059,7 +2059,7 @@ class AQtimeParser(XmlParser):
self.parse_headers()
results = self.parse_results()
self.element_end('AQtime_Results')
- return self.build_profile(results)
+ return self.build_profile(results)
def parse_headers(self):
self.element_start('HEADERS')
@@ -2163,7 +2163,7 @@ class AQtimeParser(XmlParser):
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']
@@ -2263,7 +2263,7 @@ class PstatsParser:
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),
@@ -2320,10 +2320,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:
@@ -2446,10 +2446,10 @@ 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),
)
@@ -2471,13 +2471,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),
)
@@ -2621,7 +2621,7 @@ class Main:
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
@@ -2655,7 +2655,7 @@ class Main:
fp = sys.stdin
else:
fp = open(self.args[0], 'rt')
- parser = HProfParser(fp)
+ parser = HProfParser(fp)
elif self.options.format == 'pstats':
if not self.args:
parser.error('at least a file must be specified for pstats input')
@@ -2686,7 +2686,7 @@ class Main:
parser.error('invalid format \'%s\'' % self.options.format)
self.profile = parser.parse()
-
+
if self.options.output is None:
self.output = sys.stdout
else:
@@ -2760,4 +2760,4 @@ class Main:
if __name__ == '__main__':
- Main().main() \ No newline at end of file
+ Main().main()
diff --git a/styles/style.css b/styles/style.css
index bd6446b..5006204 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: 580px;
+ 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,20 +147,20 @@ 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;
}
@@ -172,4 +172,4 @@ a.load_invocations {
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..c80d8bd 100644
--- a/templates/fileviewer.phtml
+++ b/templates/fileviewer.phtml
@@ -10,42 +10,41 @@
webgrind - fileviewer: <?php echo $file?>
</title>
<script type="text/javascript" charset="utf-8">
- $(document).ready(function() {
+ $(document).ready(function() {
$('div#'+location.hash.substr(1)).addClass('line_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==''):?>
+ <?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>
</body>
-</html> \ No newline at end of file
+</html>
diff --git a/templates/index.phtml b/templates/index.phtml
index 7c3771a..d7f3de0 100644
--- a/templates/index.phtml
+++ b/templates/index.phtml
@@ -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,24 +50,24 @@
$("#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" 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>'
+ );
+
$("#hello_message").hide();
$("#trace_view").show();
-
+
$("#function_table").trigger('update');
$("#function_table").trigger("sorton",[[[4,1]]]);
-
+
$('#callfilter').trigger('keyup');
}
);
@@ -80,13 +80,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 +95,51 @@
}
);
}
-
- 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;
}
- );
-
+ );
}
-
- 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 +147,8 @@
</table> \
';
}
-
- function callTableRow(nr,data){
+
+ function callTableRow(nr, data) {
return '<tr> \
<td>'
+($("#callinfo_area_"+data.functionNr).length ? '<img src="img/right.gif">&nbsp;&nbsp;<a href="javascript:openCallInfo('+data.functionNr+')">'+data.callerFunctionName+'</a>' : '<img src="img/blank.gif">&nbsp;&nbsp;'+data.callerFunctionName)
@@ -161,44 +157,43 @@
<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]){
+
+ 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')
+ 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 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);
+ window.scrollTo(0, areaEl.parent().offset().top);
+ }
}
-
- 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="<?php echo ini_get('xdebug.manual_url')?>/'+data.functionName.substr(5)+'" 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> \
+ <img src="img/call_'+data.humanKind+'.png" title="'+data.humanKind+'"> \
+ </td> \
<td> \
<a href="javascript:toggleCallInfo('+data.nr+')"> \
<img id="fold_marker_'+data.nr+'" src="img/right.gif">&nbsp;&nbsp;'+data.functionName+' \
@@ -212,161 +207,158 @@
</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.');
}
}
- );
-
+ );
}
-
- $(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) {
+
+ if (document.location.hash) {
update(document.location.hash.substr(1));
}
-
+
<?php if(Webgrind_Config::$checkVersion):?>
setTimeout(checkVersion,100);
<?php endif?>
-
+
$("#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 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');
+ });
+ });
});
-
</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="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><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">&nbsp;</div>
- <?php endif?>
- Copyright © 2008-2011 Jacob Oettinger &amp; 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)
+ <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">&nbsp;</div>
+ <?php endif?>
+ Copyright © 2008-2011 Jacob Oettinger &amp; Joakim Nygård. <a href="http://github.com/jokkedk/webgrind/">webgrind homepage</a>
+ </div>
+ </div>
</body>
-</html> \ No newline at end of file
+</html>