Commit 7953e21f authored by Ad Schellevis's avatar Ad Schellevis

add XDebug support switch and debugger for development

When using webgrind, use the optional graphviz package for callgraph support
parent 76329592
......@@ -893,13 +893,20 @@ function system_generate_lighty_config(
} else {
$captiveportal = ",\"mod_cgi\"";
$http_rewrite_rules = <<<EOD
### Phalcon ui and api routing
# Phalcon ui and api routing
alias.url += ( "/ui/" => "/usr/local/opnsense/www/" )
alias.url += ( "/api/" => "/usr/local/opnsense/www/" )
url.rewrite-if-not-file = ( "^/ui/(.*)$" => "/ui/index.php?_url=/$1" ,
"^/api/(.*)$" => "/api/api.php?_url=/$1"
)
EOD;
if (isset($config['system']['webgui']['enable_xdebug'])) {
$http_rewrite_rules .= <<<EOD
# Enable webgrind to analyse profiler results
alias.url += ( "/webgrind/" => "/usr/local/opnsense/contrib/webgrind/" )
EOD;
}
$captive_portal_mod_evasive = "";
$server_upload_dirs = "server.upload-dirs = ( \"{$g['upload_path']}/\", \"/tmp/\", \"/var/\" )\n";
......
......@@ -163,6 +163,20 @@ for EXT in $PHPMODULES; do
fi
done
# Enable XDebug if enabled in config
if [ ! -z `/usr/bin/grep "<enable_xdebug>1</enable_xdebug>" /conf/config.xml` ]; then
/bin/cat >>/usr/local/lib/php.ini <<EOF
extension=xdebug.so
[xdebug]
xdebug.profiler_enable_trigger = 1
xdebug.profiler_output_name = cachegrind.out.%t.%p
EOF
fi
/bin/cat >>/usr/local/lib/php.ini <<EOF
[suhosin]
suhosin.get.max_array_depth = 5000
......
Webgrind
========
Webgrind is a [Xdebug](http://www.xdebug.org) profiling web frontend in PHP5. It implements a subset of the features of [kcachegrind](http://kcachegrind.sourceforge.net/cgi-bin/show.cgi) and installs in seconds and works on all platforms. For quick'n'dirty optimizations it does the job. Here's a screenshot showing the output from profiling:
[![](http://jokke.dk/media/2008-webgrind/webgrind_small.png)](http://jokke.dk/media/2008-webgrind/webgrind_large.png)
It is possible that a larger number of kcachegrind features will be implemented in the future, bringing webgrind closer to completing one of the [suggested PHP Google Summer of Code 2008](http://wiki.php.net/gsoc/2008#xdebug_profiling_web_frontend). At this point nothing has been planned, though.
Features
--------
* Super simple, cross platform installation - obviously :)
* Track time spent in functions by self cost or inclusive cost. Inclusive cost is time inside function + calls to other functions.
* See if time is spent in internal or user functions.
* See where any function was called from and which functions it calls.
* Generate a call graph using [gprof2dot.py](https://github.com/jrfonseca/gprof2dot)
Suggestions for improvements and new features are more than welcome - this is just a start.
Mailing list is available through the [webgrind google group](http://groups.google.com/group/webgrind-general/topics).
Installation
------------
1. Download webgrind
2. Unzip package to favourite path accessible by webserver.
3. Load webgrind in browser and start profiling
See the [Installation Wiki page](https://github.com/jokkedk/webgrind/wiki/Installation) for more
Credits
-------
Webgrind is written by [Joakim Nygård](http://jokke.dk) and [Jacob Oettinger](http://oettinger.dk). It would not have been possible without the great tool that Xdebug is thanks to [Derick Rethans](http://www.derickrethans.nl).
<?php
/**
* Configuration for webgrind
* @author Jacob Oettinger
* @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/local/bin/python2.7';
/**
* 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]';
#########################
# BELOW NOT FOR EDITING #
#########################
/**
* 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;
}
/**
* 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
return Webgrind_Config::xdebugOutputDir();
}
return realpath(sys_get_temp_dir()).'/';
}
}
<?php
/**
* @author Jacob Oettinger
* @author Joakim Nygård
*/
class Webgrind_MasterConfig
{
static $webgrindVersion = '1.1';
}
require './config.php';
require './library/FileHandler.php';
// TODO: Errorhandling:
// No files, outputdir not writable
set_time_limit(0);
// Make sure we have a timezone for date functions.
if (ini_get('date.timezone') == '')
date_default_timezone_set( Webgrind_Config::$defaultTimezone );
try {
switch(get('op')){
case 'file_list':
echo json_encode(Webgrind_FileHandler::getInstance()->getTraceList());
break;
case 'function_list':
$dataFile = get('dataFile');
if($dataFile=='0'){
$files = Webgrind_FileHandler::getInstance()->getTraceList();
$dataFile = $files[0]['filename'];
}
$reader = Webgrind_FileHandler::getInstance()->getTraceReader($dataFile, get('costFormat', Webgrind_Config::$defaultCostformat));
$functions = array();
$shownTotal = 0;
$breakdown = array('internal' => 0, 'procedural' => 0, 'class' => 0, 'include' => 0);
for($i=0;$i<$reader->getFunctionCount();$i++) {
$functionInfo = $reader->getFunctionInfo($i);
if (false !== strpos($functionInfo['functionName'], 'php::')) {
$breakdown['internal'] += $functionInfo['summedSelfCost'];
$humanKind = 'internal';
} elseif (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'];
$humanKind = 'include';
} else {
if (false !== strpos($functionInfo['functionName'], '->') || false !== strpos($functionInfo['functionName'], '::')) {
$breakdown['class'] += $functionInfo['summedSelfCost'];
$humanKind = 'class';
} else {
$breakdown['procedural'] += $functionInfo['summedSelfCost'];
$humanKind = 'procedural';
}
}
if (!(int)get('hideInternals', 0) || strpos($functionInfo['functionName'], 'php::') === false) {
$shownTotal += $functionInfo['summedSelfCost'];
$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'];
$function['file'] = urlencode($function['file']);
$result['functions'][] = $function;
if($remainingCost<0)
break;
}
$result['summedInvocationCount'] = $reader->getFunctionCount();
$result['summedRunTime'] = $reader->formatCost($reader->getHeader('summary'), 'msec');
$result['dataFile'] = $dataFile;
$result['invokeUrl'] = $reader->getHeader('cmd');
$result['runs'] = $reader->getHeader('runs');
$result['breakdown'] = $breakdown;
$result['mtime'] = date(Webgrind_Config::$dateFormat,filemtime(Webgrind_Config::xdebugOutputDir().$dataFile));
$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');
$function = $reader->getFunctionInfo($functionNr);
$result = array('calledFrom'=>array(), 'subCalls'=>array());
$foundInvocations = 0;
for($i=0;$i<$function['calledFromInfoCount'];$i++){
$invo = $reader->getCalledFromInfo($functionNr, $i);
$foundInvocations += $invo['callCount'];
$callerInfo = $reader->getFunctionInfo($invo['functionNr']);
$invo['file'] = urlencode($callerInfo['file']);
$invo['callerFunctionName'] = $callerInfo['functionName'];
$result['calledFrom'][] = $invo;
}
$result['calledByHost'] = ($foundInvocations<$function['invocationCount']);
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);
break;
case 'fileviewer':
$file = get('file');
$line = get('line');
if($file && $file!=''){
$message = '';
if(!file_exists($file)){
$message = $file.' does not exist.';
} else if(!is_readable($file)){
$message = $file.' is not readable.';
} 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'){
$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().''.$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>';
}
if (!file_exists(Webgrind_Config::xdebugOutputDir()) || !is_readable(Webgrind_Config::xdebugOutputDir())) {
$welcome .= 'Webgrind $profilerDir does not exist or is not readable: <code>'.Webgrind_Config::xdebugOutputDir().'</code><br>';
}
if ($welcome == '') {
$welcome = 'Select a cachegrind file above<br>(looking in <code>'.Webgrind_Config::xdebugOutputDir().'</code> for files matching <code>'.Webgrind_Config::xdebugOutputFormat().'</code>)';
}
require 'templates/index.phtml';
}
} catch (Exception $e) {
echo json_encode(array('error' => $e->getMessage().'<br>'.$e->getFile().', line '.$e->getLine()));
return;
}
function get($param, $default=false){
return (isset($_GET[$param])? $_GET[$param] : $default);
}
function costCmp($a, $b){
$a = $a['summedSelfCost'];
$b = $b['summedSelfCost'];
if ($a == $b) {
return 0;
}
return ($a > $b) ? -1 : 1;
}
/*
* jQuery blockUI plugin
* Version 1.33 (09/14/2007)
* @requires jQuery v1.1.1
*
* $Id$
*
* Examples at: http://malsup.com/jquery/block/
* Copyright (c) 2007 M. Alsup
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*/
(function($) {
/**
* blockUI provides a mechanism for blocking user interaction with a page (or parts of a page).
* This can be an effective way to simulate synchronous behavior during ajax operations without
* locking the browser. It will prevent user operations for the current page while it is
* active ane will return the page to normal when it is deactivate. blockUI accepts the following
* two optional arguments:
*
* message (String|Element|jQuery): The message to be displayed while the UI is blocked. The message
* argument can be a plain text string like "Processing...", an HTML string like
* "<h1><img src="busy.gif" /> Please wait...</h1>", a DOM element, or a jQuery object.
* The default message is "<h1>Please wait...</h1>"
*
* css (Object): Object which contains css property/values to override the default styles of
* the message. Use this argument if you wish to override the default
* styles. The css Object should be in a format suitable for the jQuery.css
* function. For example:
* $.blockUI({
* backgroundColor: '#ff8',
* border: '5px solid #f00,
* fontWeight: 'bold'
* });
*
* The default blocking message used when blocking the entire page is "<h1>Please wait...</h1>"
* but this can be overridden by assigning a value to $.blockUI.defaults.pageMessage in your
* own code. For example:
*
* $.blockUI.defaults.pageMessage = "<h1>Bitte Wartezeit</h1>";
*
* The default message styling can also be overridden. For example:
*
* $.extend($.blockUI.defaults.pageMessageCSS, { color: '#00a', backgroundColor: '#0f0' });
*
* The default styles work well for simple messages like "Please wait", but for longer messages
* style overrides may be necessary.
*
* @example $.blockUI();
* @desc prevent user interaction with the page (and show the default message of 'Please wait...')
*
* @example $.blockUI( { backgroundColor: '#f00', color: '#fff'} );
* @desc prevent user interaction and override the default styles of the message to use a white on red color scheme
*
* @example $.blockUI('Processing...');
* @desc prevent user interaction and display the message "Processing..." instead of the default message
*
* @name blockUI
* @param String|jQuery|Element message Message to display while the UI is blocked
* @param Object css Style object to control look of the message
* @cat Plugins/blockUI
*/
$.blockUI = function(msg, css, opts) {
$.blockUI.impl.install(window, msg, css, opts);
};
// expose version number so other plugins can interogate
$.blockUI.version = 1.33;
/**
* unblockUI removes the UI block that was put in place by blockUI
*
* @example $.unblockUI();
* @desc unblocks the page
*
* @name unblockUI
* @cat Plugins/blockUI
*/
$.unblockUI = function(opts) {
$.blockUI.impl.remove(window, opts);
};
/**
* Blocks user interaction with the selected elements. (Hat tip: Much of
* this logic comes from Brandon Aaron's bgiframe plugin. Thanks, Brandon!)
* By default, no message is displayed when blocking elements.
*
* @example $('div.special').block();
* @desc prevent user interaction with all div elements with the 'special' class.
*
* @example $('div.special').block('Please wait');
* @desc prevent user interaction with all div elements with the 'special' class
* and show a message over the blocked content.
*
* @name block
* @type jQuery
* @param String|jQuery|Element message Message to display while the element is blocked
* @param Object css Style object to control look of the message
* @cat Plugins/blockUI
*/
$.fn.block = function(msg, css, opts) {
return this.each(function() {
if (!this.$pos_checked) {
if ($.css(this,"position") == 'static')
this.style.position = 'relative';
if ($.browser.msie) this.style.zoom = 1; // force 'hasLayout' in IE
this.$pos_checked = 1;
}
$.blockUI.impl.install(this, msg, css, opts);
});
};
/**
* Unblocks content that was blocked by "block()"
*
* @example $('div.special').unblock();
* @desc unblocks all div elements with the 'special' class.
*
* @name unblock
* @type jQuery
* @cat Plugins/blockUI
*/
$.fn.unblock = function(opts) {
return this.each(function() {
$.blockUI.impl.remove(this, opts);
});
};
/**
* displays the first matched element in a "display box" above a page overlay.
*
* @example $('#myImage').displayBox();
* @desc displays "myImage" element in a box
*
* @name displayBox
* @type jQuery
* @cat Plugins/blockUI
*/
$.fn.displayBox = function(css, fn, isFlash) {
var msg = this[0];
if (!msg) return;
var $msg = $(msg);
css = css || {};
var w = $msg.width() || $msg.attr('width') || css.width || $.blockUI.defaults.displayBoxCSS.width;
var h = $msg.height() || $msg.attr('height') || css.height || $.blockUI.defaults.displayBoxCSS.height ;
if (w[w.length-1] == '%') {
var ww = document.documentElement.clientWidth || document.body.clientWidth;
w = parseInt(w) || 100;
w = (w * ww) / 100;
}
if (h[h.length-1] == '%') {
var hh = document.documentElement.clientHeight || document.body.clientHeight;
h = parseInt(h) || 100;
h = (h * hh) / 100;
}
var ml = '-' + parseInt(w)/2 + 'px';
var mt = '-' + parseInt(h)/2 + 'px';
// supress opacity on overlay if displaying flash content on mac/ff platform
var ua = navigator.userAgent.toLowerCase();
var opts = {
displayMode: fn || 1,
noalpha: isFlash && /mac/.test(ua) && /firefox/.test(ua)
};
$.blockUI.impl.install(window, msg, { width: w, height: h, marginTop: mt, marginLeft: ml }, opts);
};
// override these in your code to change the default messages and styles
$.blockUI.defaults = {
// the message displayed when blocking the entire page
pageMessage: '<h1>Please wait...</h1>',
// the message displayed when blocking an element
elementMessage: '', // none
// styles for the overlay iframe
overlayCSS: { backgroundColor: '#fff', opacity: '0.5' },
// styles for the message when blocking the entire page
pageMessageCSS: { width:'250px', margin:'-50px 0 0 -125px', top:'50%', left:'50%', textAlign:'center', color:'#000', backgroundColor:'#fff', border:'3px solid #aaa' },
// styles for the message when blocking an element
elementMessageCSS: { width:'250px', padding:'10px', textAlign:'center', backgroundColor:'#fff'},
// styles for the displayBox
displayBoxCSS: { width: '400px', height: '400px', top:'50%', left:'50%' },
// allow body element to be stetched in ie6
ie6Stretch: 1,
// supress tab nav from leaving blocking content?
allowTabToLeave: 0,
// Title attribute for overlay when using displayBox
closeMessage: 'Click to close',
// use fadeOut effect when unblocking (can be overridden on unblock call)
fadeOut: 1,
// fadeOut transition time in millis
fadeTime: 400
};
// the gory details
$.blockUI.impl = {
box: null,
boxCallback: null,
pageBlock: null,
pageBlockEls: [],
op8: window.opera && window.opera.version() < 9,
ie6: $.browser.msie && /MSIE 6.0/.test(navigator.userAgent),
install: function(el, msg, css, opts) {
opts = opts || {};
this.boxCallback = typeof opts.displayMode == 'function' ? opts.displayMode : null;
this.box = opts.displayMode ? msg : null;
var full = (el == window);
// use logical settings for opacity support based on browser but allow overrides via opts arg
var noalpha = this.op8 || $.browser.mozilla && /Linux/.test(navigator.platform);
if (typeof opts.alphaOverride != 'undefined')
noalpha = opts.alphaOverride == 0 ? 1 : 0;
if (full && this.pageBlock) this.remove(window, {fadeOut:0});
// check to see if we were only passed the css object (a literal)
if (msg && typeof msg == 'object' && !msg.jquery && !msg.nodeType) {
css = msg;
msg = null;
}
msg = msg ? (msg.nodeType ? $(msg) : msg) : full ? $.blockUI.defaults.pageMessage : $.blockUI.defaults.elementMessage;
if (opts.displayMode)
var basecss = jQuery.extend({}, $.blockUI.defaults.displayBoxCSS);
else
var basecss = jQuery.extend({}, full ? $.blockUI.defaults.pageMessageCSS : $.blockUI.defaults.elementMessageCSS);
css = jQuery.extend(basecss, css || {});
var f = ($.browser.msie) ? $('<iframe class="blockUI" style="z-index:1000;border:none;margin:0;padding:0;position:absolute;width:100%;height:100%;top:0;left:0" src="javascript:false;"></iframe>')
: $('<div class="blockUI" style="display:none"></div>');
var w = $('<div class="blockUI" style="z-index:1001;cursor:wait;border:none;margin:0;padding:0;width:100%;height:100%;top:0;left:0"></div>');
var m = full ? $('<div class="blockUI blockMsg" style="z-index:1002;cursor:wait;padding:0;position:fixed"></div>')
: $('<div class="blockUI" style="display:none;z-index:1002;cursor:wait;position:absolute"></div>');
w.css('position', full ? 'fixed' : 'absolute');
if (msg) m.css(css);
if (!noalpha) w.css($.blockUI.defaults.overlayCSS);
if (this.op8) w.css({ width:''+el.clientWidth,height:''+el.clientHeight }); // lame
if ($.browser.msie) f.css('opacity','0.0');
$([f[0],w[0],m[0]]).appendTo(full ? 'body' : el);
// ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling)
var expr = $.browser.msie && (!$.boxModel || $('object,embed', full ? null : el).length > 0);
if (this.ie6 || expr) {
// stretch content area if it's short
if (full && $.blockUI.defaults.ie6Stretch && $.boxModel)
$('html,body').css('height','100%');
// fix ie6 problem when blocked element has a border width
if ((this.ie6 || !$.boxModel) && !full) {
var t = this.sz(el,'borderTopWidth'), l = this.sz(el,'borderLeftWidth');
var fixT = t ? '(0 - '+t+')' : 0;
var fixL = l ? '(0 - '+l+')' : 0;
}
// simulate fixed position
$.each([f,w,m], function(i,o) {
var s = o[0].style;
s.position = 'absolute';
if (i < 2) {
full ? s.setExpression('height','document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + "px"')
: s.setExpression('height','this.parentNode.offsetHeight + "px"');
full ? s.setExpression('width','jQuery.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"')
: s.setExpression('width','this.parentNode.offsetWidth + "px"');
if (fixL) s.setExpression('left', fixL);
if (fixT) s.setExpression('top', fixT);
}
else {
if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"');
s.marginTop = 0;
}
});
}
if (opts.displayMode) {
w.css('cursor','default').attr('title', $.blockUI.defaults.closeMessage);
m.css('cursor','default');
$([f[0],w[0],m[0]]).removeClass('blockUI').addClass('displayBox');
$().click($.blockUI.impl.boxHandler).bind('keypress', $.blockUI.impl.boxHandler);
}
else
this.bind(1, el);
m.append(msg).show();
if (msg.jquery) msg.show();
if (opts.displayMode) return;
if (full) {
this.pageBlock = m[0];
this.pageBlockEls = $(':input:enabled:visible',this.pageBlock);
setTimeout(this.focus, 20);
}
else this.center(m[0]);
},
remove: function(el, opts) {
var o = $.extend({}, $.blockUI.defaults, opts);
this.bind(0, el);
var full = el == window;
var els = full ? $('body').children().filter('.blockUI') : $('.blockUI', el);
if (full) this.pageBlock = this.pageBlockEls = null;
if (o.fadeOut) {
els.fadeOut(o.fadeTime, function() {
if (this.parentNode) this.parentNode.removeChild(this);
});
}
else els.remove();
},
boxRemove: function(el) {
$().unbind('click',$.blockUI.impl.boxHandler).unbind('keypress', $.blockUI.impl.boxHandler);
if (this.boxCallback)
this.boxCallback(this.box);
$('body .displayBox').hide().remove();
},
// event handler to suppress keyboard/mouse events when blocking
handler: function(e) {
if (e.keyCode && e.keyCode == 9) {
if ($.blockUI.impl.pageBlock && !$.blockUI.defaults.allowTabToLeave) {
var els = $.blockUI.impl.pageBlockEls;
var fwd = !e.shiftKey && e.target == els[els.length-1];
var back = e.shiftKey && e.target == els[0];
if (fwd || back) {
setTimeout(function(){$.blockUI.impl.focus(back)},10);
return false;
}
}
}
if ($(e.target).parents('div.blockMsg').length > 0)
return true;
return $(e.target).parents().children().filter('div.blockUI').length == 0;
},
boxHandler: function(e) {
if ((e.keyCode && e.keyCode == 27) || (e.type == 'click' && $(e.target).parents('div.blockMsg').length == 0))
$.blockUI.impl.boxRemove();
return true;
},
// bind/unbind the handler
bind: function(b, el) {
var full = el == window;
// don't bother unbinding if there is nothing to unbind
if (!b && (full && !this.pageBlock || !full && !el.$blocked)) return;
if (!full) el.$blocked = b;
var $e = $(el).find('a,:input');
$.each(['mousedown','mouseup','keydown','keypress','click'], function(i,o) {
$e[b?'bind':'unbind'](o, $.blockUI.impl.handler);
});
},
focus: function(back) {
if (!$.blockUI.impl.pageBlockEls) return;
var e = $.blockUI.impl.pageBlockEls[back===true ? $.blockUI.impl.pageBlockEls.length-1 : 0];
if (e) e.focus();
},
center: function(el) {
var p = el.parentNode, s = el.style;
var l = ((p.offsetWidth - el.offsetWidth)/2) - this.sz(p,'borderLeftWidth');
var t = ((p.offsetHeight - el.offsetHeight)/2) - this.sz(p,'borderTopWidth');
s.left = l > 0 ? (l+'px') : '0';
s.top = t > 0 ? (t+'px') : '0';
},
sz: function(el, p) { return parseInt($.css(el,p))||0; }
};
})(jQuery);
/*
* jQuery 1.2.3 - New Wave Javascript
*
* Copyright (c) 2008 John Resig (jquery.com)
* Dual licensed under the MIT (MIT-LICENSE.txt)
* and GPL (GPL-LICENSE.txt) licenses.
*
* $Date: 2008-02-06 00:21:25 -0500 (Wed, 06 Feb 2008) $
* $Rev: 4663 $
*/
eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('(J(){7(1e.3N)L w=1e.3N;L E=1e.3N=J(a,b){K 1B E.2l.4T(a,b)};7(1e.$)L D=1e.$;1e.$=E;L u=/^[^<]*(<(.|\\s)+>)[^>]*$|^#(\\w+)$/;L G=/^.[^:#\\[\\.]*$/;E.1n=E.2l={4T:J(d,b){d=d||T;7(d.15){6[0]=d;6.M=1;K 6}N 7(1o d=="25"){L c=u.2O(d);7(c&&(c[1]||!b)){7(c[1])d=E.4a([c[1]],b);N{L a=T.5J(c[3]);7(a)7(a.2w!=c[3])K E().2s(d);N{6[0]=a;6.M=1;K 6}N d=[]}}N K 1B E(b).2s(d)}N 7(E.1q(d))K 1B E(T)[E.1n.21?"21":"3U"](d);K 6.6E(d.1k==1M&&d||(d.5h||d.M&&d!=1e&&!d.15&&d[0]!=10&&d[0].15)&&E.2I(d)||[d])},5h:"1.2.3",87:J(){K 6.M},M:0,22:J(a){K a==10?E.2I(6):6[a]},2F:J(b){L a=E(b);a.54=6;K a},6E:J(a){6.M=0;1M.2l.1g.1i(6,a);K 6},R:J(a,b){K E.R(6,a,b)},4X:J(b){L a=-1;6.R(J(i){7(6==b)a=i});K a},1J:J(c,a,b){L d=c;7(c.1k==4e)7(a==10)K 6.M&&E[b||"1J"](6[0],c)||10;N{d={};d[c]=a}K 6.R(J(i){Q(c 1p d)E.1J(b?6.W:6,c,E.1l(6,d[c],b,i,c))})},1j:J(b,a){7((b==\'27\'||b==\'1R\')&&2M(a)<0)a=10;K 6.1J(b,a,"2o")},1u:J(b){7(1o b!="3V"&&b!=V)K 6.4x().3t((6[0]&&6[0].2i||T).5r(b));L a="";E.R(b||6,J(){E.R(6.3p,J(){7(6.15!=8)a+=6.15!=1?6.6K:E.1n.1u([6])})});K a},5m:J(b){7(6[0])E(b,6[0].2i).5k().3o(6[0]).2c(J(){L a=6;2b(a.1C)a=a.1C;K a}).3t(6);K 6},8w:J(a){K 6.R(J(){E(6).6z().5m(a)})},8p:J(a){K 6.R(J(){E(6).5m(a)})},3t:J(){K 6.3O(18,P,S,J(a){7(6.15==1)6.38(a)})},6q:J(){K 6.3O(18,P,P,J(a){7(6.15==1)6.3o(a,6.1C)})},6o:J(){K 6.3O(18,S,S,J(a){6.1a.3o(a,6)})},5a:J(){K 6.3O(18,S,P,J(a){6.1a.3o(a,6.2B)})},3h:J(){K 6.54||E([])},2s:J(b){L c=E.2c(6,J(a){K E.2s(b,a)});K 6.2F(/[^+>] [^+>]/.17(b)||b.1f("..")>-1?E.57(c):c)},5k:J(e){L f=6.2c(J(){7(E.14.1d&&!E.3E(6)){L a=6.69(P),4Y=T.3s("1x");4Y.38(a);K E.4a([4Y.3d])[0]}N K 6.69(P)});L d=f.2s("*").4R().R(J(){7(6[F]!=10)6[F]=V});7(e===P)6.2s("*").4R().R(J(i){7(6.15==3)K;L c=E.O(6,"2R");Q(L a 1p c)Q(L b 1p c[a])E.16.1b(d[i],a,c[a][b],c[a][b].O)});K f},1E:J(b){K 6.2F(E.1q(b)&&E.3y(6,J(a,i){K b.1P(a,i)})||E.3e(b,6))},56:J(b){7(b.1k==4e)7(G.17(b))K 6.2F(E.3e(b,6,P));N b=E.3e(b,6);L a=b.M&&b[b.M-1]!==10&&!b.15;K 6.1E(J(){K a?E.33(6,b)<0:6!=b})},1b:J(a){K!a?6:6.2F(E.37(6.22(),a.1k==4e?E(a).22():a.M!=10&&(!a.12||E.12(a,"3u"))?a:[a]))},3H:J(a){K a?E.3e(a,6).M>0:S},7j:J(a){K 6.3H("."+a)},5O:J(b){7(b==10){7(6.M){L c=6[0];7(E.12(c,"2k")){L e=c.3T,5I=[],11=c.11,2X=c.U=="2k-2X";7(e<0)K V;Q(L i=2X?e:0,2f=2X?e+1:11.M;i<2f;i++){L d=11[i];7(d.2p){b=E.14.1d&&!d.9J.1A.9y?d.1u:d.1A;7(2X)K b;5I.1g(b)}}K 5I}N K(6[0].1A||"").1r(/\\r/g,"")}K 10}K 6.R(J(){7(6.15!=1)K;7(b.1k==1M&&/5u|5t/.17(6.U))6.3k=(E.33(6.1A,b)>=0||E.33(6.31,b)>=0);N 7(E.12(6,"2k")){L a=b.1k==1M?b:[b];E("98",6).R(J(){6.2p=(E.33(6.1A,a)>=0||E.33(6.1u,a)>=0)});7(!a.M)6.3T=-1}N 6.1A=b})},3q:J(a){K a==10?(6.M?6[0].3d:V):6.4x().3t(a)},6S:J(a){K 6.5a(a).1V()},6Z:J(i){K 6.2K(i,i+1)},2K:J(){K 6.2F(1M.2l.2K.1i(6,18))},2c:J(b){K 6.2F(E.2c(6,J(a,i){K b.1P(a,i,a)}))},4R:J(){K 6.1b(6.54)},O:J(d,b){L a=d.23(".");a[1]=a[1]?"."+a[1]:"";7(b==V){L c=6.5n("8P"+a[1]+"!",[a[0]]);7(c==10&&6.M)c=E.O(6[0],d);K c==V&&a[1]?6.O(a[0]):c}N K 6.1N("8K"+a[1]+"!",[a[0],b]).R(J(){E.O(6,d,b)})},35:J(a){K 6.R(J(){E.35(6,a)})},3O:J(g,f,h,d){L e=6.M>1,3n;K 6.R(J(){7(!3n){3n=E.4a(g,6.2i);7(h)3n.8D()}L b=6;7(f&&E.12(6,"1O")&&E.12(3n[0],"4v"))b=6.3S("1U")[0]||6.38(6.2i.3s("1U"));L c=E([]);E.R(3n,J(){L a=e?E(6).5k(P)[0]:6;7(E.12(a,"1m")){c=c.1b(a)}N{7(a.15==1)c=c.1b(E("1m",a).1V());d.1P(b,a)}});c.R(6A)})}};E.2l.4T.2l=E.2l;J 6A(i,a){7(a.3Q)E.3P({1c:a.3Q,3l:S,1H:"1m"});N E.5g(a.1u||a.6x||a.3d||"");7(a.1a)a.1a.34(a)}E.1s=E.1n.1s=J(){L b=18[0]||{},i=1,M=18.M,5c=S,11;7(b.1k==8d){5c=b;b=18[1]||{};i=2}7(1o b!="3V"&&1o b!="J")b={};7(M==1){b=6;i=0}Q(;i<M;i++)7((11=18[i])!=V)Q(L a 1p 11){7(b===11[a])6w;7(5c&&11[a]&&1o 11[a]=="3V"&&b[a]&&!11[a].15)b[a]=E.1s(b[a],11[a]);N 7(11[a]!=10)b[a]=11[a]}K b};L F="3N"+(1B 3v()).3L(),6t=0,5b={};L H=/z-?4X|86-?84|1w|6k|7Z-?1R/i;E.1s({7Y:J(a){1e.$=D;7(a)1e.3N=w;K E},1q:J(a){K!!a&&1o a!="25"&&!a.12&&a.1k!=1M&&/J/i.17(a+"")},3E:J(a){K a.1F&&!a.1h||a.28&&a.2i&&!a.2i.1h},5g:J(a){a=E.3g(a);7(a){L b=T.3S("6f")[0]||T.1F,1m=T.3s("1m");1m.U="1u/4m";7(E.14.1d)1m.1u=a;N 1m.38(T.5r(a));b.38(1m);b.34(1m)}},12:J(b,a){K b.12&&b.12.2E()==a.2E()},1T:{},O:J(c,d,b){c=c==1e?5b:c;L a=c[F];7(!a)a=c[F]=++6t;7(d&&!E.1T[a])E.1T[a]={};7(b!=10)E.1T[a][d]=b;K d?E.1T[a][d]:a},35:J(c,b){c=c==1e?5b:c;L a=c[F];7(b){7(E.1T[a]){2V E.1T[a][b];b="";Q(b 1p E.1T[a])1Q;7(!b)E.35(c)}}N{1S{2V c[F]}1X(e){7(c.52)c.52(F)}2V E.1T[a]}},R:J(c,a,b){7(b){7(c.M==10){Q(L d 1p c)7(a.1i(c[d],b)===S)1Q}N Q(L i=0,M=c.M;i<M;i++)7(a.1i(c[i],b)===S)1Q}N{7(c.M==10){Q(L d 1p c)7(a.1P(c[d],d,c[d])===S)1Q}N Q(L i=0,M=c.M,1A=c[0];i<M&&a.1P(1A,i,1A)!==S;1A=c[++i]){}}K c},1l:J(b,a,c,i,d){7(E.1q(a))a=a.1P(b,i);K a&&a.1k==51&&c=="2o"&&!H.17(d)?a+"2S":a},1t:{1b:J(c,b){E.R((b||"").23(/\\s+/),J(i,a){7(c.15==1&&!E.1t.3Y(c.1t,a))c.1t+=(c.1t?" ":"")+a})},1V:J(c,b){7(c.15==1)c.1t=b!=10?E.3y(c.1t.23(/\\s+/),J(a){K!E.1t.3Y(b,a)}).6a(" "):""},3Y:J(b,a){K E.33(a,(b.1t||b).3X().23(/\\s+/))>-1}},68:J(b,c,a){L e={};Q(L d 1p c){e[d]=b.W[d];b.W[d]=c[d]}a.1P(b);Q(L d 1p c)b.W[d]=e[d]},1j:J(d,e,c){7(e=="27"||e=="1R"){L b,46={43:"4W",4U:"1Z",19:"3D"},3c=e=="27"?["7O","7M"]:["7J","7I"];J 5E(){b=e=="27"?d.7H:d.7F;L a=0,2N=0;E.R(3c,J(){a+=2M(E.2o(d,"7E"+6,P))||0;2N+=2M(E.2o(d,"2N"+6+"5X",P))||0});b-=24.7C(a+2N)}7(E(d).3H(":4d"))5E();N E.68(d,46,5E);K 24.2f(0,b)}K E.2o(d,e,c)},2o:J(e,k,j){L d;J 3x(b){7(!E.14.2d)K S;L a=T.4c.4K(b,V);K!a||a.4M("3x")==""}7(k=="1w"&&E.14.1d){d=E.1J(e.W,"1w");K d==""?"1":d}7(E.14.2z&&k=="19"){L c=e.W.50;e.W.50="0 7r 7o";e.W.50=c}7(k.1D(/4g/i))k=y;7(!j&&e.W&&e.W[k])d=e.W[k];N 7(T.4c&&T.4c.4K){7(k.1D(/4g/i))k="4g";k=k.1r(/([A-Z])/g,"-$1").2h();L h=T.4c.4K(e,V);7(h&&!3x(e))d=h.4M(k);N{L f=[],2C=[];Q(L a=e;a&&3x(a);a=a.1a)2C.4J(a);Q(L i=0;i<2C.M;i++)7(3x(2C[i])){f[i]=2C[i].W.19;2C[i].W.19="3D"}d=k=="19"&&f[2C.M-1]!=V?"2H":(h&&h.4M(k))||"";Q(L i=0;i<f.M;i++)7(f[i]!=V)2C[i].W.19=f[i]}7(k=="1w"&&d=="")d="1"}N 7(e.4n){L g=k.1r(/\\-(\\w)/g,J(a,b){K b.2E()});d=e.4n[k]||e.4n[g];7(!/^\\d+(2S)?$/i.17(d)&&/^\\d/.17(d)){L l=e.W.26,3K=e.3K.26;e.3K.26=e.4n.26;e.W.26=d||0;d=e.W.7f+"2S";e.W.26=l;e.3K.26=3K}}K d},4a:J(l,h){L k=[];h=h||T;7(1o h.3s==\'10\')h=h.2i||h[0]&&h[0].2i||T;E.R(l,J(i,d){7(!d)K;7(d.1k==51)d=d.3X();7(1o d=="25"){d=d.1r(/(<(\\w+)[^>]*?)\\/>/g,J(b,a,c){K c.1D(/^(aa|a6|7e|a5|4D|7a|a0|3m|9W|9U|9S)$/i)?b:a+"></"+c+">"});L f=E.3g(d).2h(),1x=h.3s("1x");L e=!f.1f("<9P")&&[1,"<2k 74=\'74\'>","</2k>"]||!f.1f("<9M")&&[1,"<73>","</73>"]||f.1D(/^<(9G|1U|9E|9B|9x)/)&&[1,"<1O>","</1O>"]||!f.1f("<4v")&&[2,"<1O><1U>","</1U></1O>"]||(!f.1f("<9w")||!f.1f("<9v"))&&[3,"<1O><1U><4v>","</4v></1U></1O>"]||!f.1f("<7e")&&[2,"<1O><1U></1U><6V>","</6V></1O>"]||E.14.1d&&[1,"1x<1x>","</1x>"]||[0,"",""];1x.3d=e[1]+d+e[2];2b(e[0]--)1x=1x.5o;7(E.14.1d){L g=!f.1f("<1O")&&f.1f("<1U")<0?1x.1C&&1x.1C.3p:e[1]=="<1O>"&&f.1f("<1U")<0?1x.3p:[];Q(L j=g.M-1;j>=0;--j)7(E.12(g[j],"1U")&&!g[j].3p.M)g[j].1a.34(g[j]);7(/^\\s/.17(d))1x.3o(h.5r(d.1D(/^\\s*/)[0]),1x.1C)}d=E.2I(1x.3p)}7(d.M===0&&(!E.12(d,"3u")&&!E.12(d,"2k")))K;7(d[0]==10||E.12(d,"3u")||d.11)k.1g(d);N k=E.37(k,d)});K k},1J:J(d,e,c){7(!d||d.15==3||d.15==8)K 10;L f=E.3E(d)?{}:E.46;7(e=="2p"&&E.14.2d)d.1a.3T;7(f[e]){7(c!=10)d[f[e]]=c;K d[f[e]]}N 7(E.14.1d&&e=="W")K E.1J(d.W,"9u",c);N 7(c==10&&E.14.1d&&E.12(d,"3u")&&(e=="9r"||e=="9o"))K d.9m(e).6K;N 7(d.28){7(c!=10){7(e=="U"&&E.12(d,"4D")&&d.1a)6Q"U 9i 9h\'t 9g 9e";d.9b(e,""+c)}7(E.14.1d&&/6O|3Q/.17(e)&&!E.3E(d))K d.4z(e,2);K d.4z(e)}N{7(e=="1w"&&E.14.1d){7(c!=10){d.6k=1;d.1E=(d.1E||"").1r(/6M\\([^)]*\\)/,"")+(2M(c).3X()=="96"?"":"6M(1w="+c*6L+")")}K d.1E&&d.1E.1f("1w=")>=0?(2M(d.1E.1D(/1w=([^)]*)/)[1])/6L).3X():""}e=e.1r(/-([a-z])/95,J(a,b){K b.2E()});7(c!=10)d[e]=c;K d[e]}},3g:J(a){K(a||"").1r(/^\\s+|\\s+$/g,"")},2I:J(b){L a=[];7(1o b!="93")Q(L i=0,M=b.M;i<M;i++)a.1g(b[i]);N a=b.2K(0);K a},33:J(b,a){Q(L i=0,M=a.M;i<M;i++)7(a[i]==b)K i;K-1},37:J(a,b){7(E.14.1d){Q(L i=0;b[i];i++)7(b[i].15!=8)a.1g(b[i])}N Q(L i=0;b[i];i++)a.1g(b[i]);K a},57:J(a){L c=[],2r={};1S{Q(L i=0,M=a.M;i<M;i++){L b=E.O(a[i]);7(!2r[b]){2r[b]=P;c.1g(a[i])}}}1X(e){c=a}K c},3y:J(c,a,d){L b=[];Q(L i=0,M=c.M;i<M;i++)7(!d&&a(c[i],i)||d&&!a(c[i],i))b.1g(c[i]);K b},2c:J(d,a){L c=[];Q(L i=0,M=d.M;i<M;i++){L b=a(d[i],i);7(b!==V&&b!=10){7(b.1k!=1M)b=[b];c=c.71(b)}}K c}});L v=8Y.8W.2h();E.14={5K:(v.1D(/.+(?:8T|8S|8R|8O)[\\/: ]([\\d.]+)/)||[])[1],2d:/77/.17(v),2z:/2z/.17(v),1d:/1d/.17(v)&&!/2z/.17(v),48:/48/.17(v)&&!/(8L|77)/.17(v)};L y=E.14.1d?"6H":"75";E.1s({8I:!E.14.1d||T.6F=="79",46:{"Q":"8F","8E":"1t","4g":y,75:y,6H:y,3d:"3d",1t:"1t",1A:"1A",2Y:"2Y",3k:"3k",8C:"8B",2p:"2p",8A:"8z",3T:"3T",6C:"6C",28:"28",12:"12"}});E.R({6B:J(a){K a.1a},8y:J(a){K E.4u(a,"1a")},8x:J(a){K E.2Z(a,2,"2B")},8v:J(a){K E.2Z(a,2,"4t")},8u:J(a){K E.4u(a,"2B")},8t:J(a){K E.4u(a,"4t")},8s:J(a){K E.5i(a.1a.1C,a)},8r:J(a){K E.5i(a.1C)},6z:J(a){K E.12(a,"8q")?a.8o||a.8n.T:E.2I(a.3p)}},J(c,d){E.1n[c]=J(b){L a=E.2c(6,d);7(b&&1o b=="25")a=E.3e(b,a);K 6.2F(E.57(a))}});E.R({6y:"3t",8m:"6q",3o:"6o",8l:"5a",8k:"6S"},J(c,b){E.1n[c]=J(){L a=18;K 6.R(J(){Q(L i=0,M=a.M;i<M;i++)E(a[i])[b](6)})}});E.R({8j:J(a){E.1J(6,a,"");7(6.15==1)6.52(a)},8i:J(a){E.1t.1b(6,a)},8h:J(a){E.1t.1V(6,a)},8g:J(a){E.1t[E.1t.3Y(6,a)?"1V":"1b"](6,a)},1V:J(a){7(!a||E.1E(a,[6]).r.M){E("*",6).1b(6).R(J(){E.16.1V(6);E.35(6)});7(6.1a)6.1a.34(6)}},4x:J(){E(">*",6).1V();2b(6.1C)6.34(6.1C)}},J(a,b){E.1n[a]=J(){K 6.R(b,18)}});E.R(["8f","5X"],J(i,c){L b=c.2h();E.1n[b]=J(a){K 6[0]==1e?E.14.2z&&T.1h["5e"+c]||E.14.2d&&1e["8e"+c]||T.6F=="79"&&T.1F["5e"+c]||T.1h["5e"+c]:6[0]==T?24.2f(24.2f(T.1h["5d"+c],T.1F["5d"+c]),24.2f(T.1h["5L"+c],T.1F["5L"+c])):a==10?(6.M?E.1j(6[0],b):V):6.1j(b,a.1k==4e?a:a+"2S")}});L C=E.14.2d&&4s(E.14.5K)<8c?"(?:[\\\\w*4r-]|\\\\\\\\.)":"(?:[\\\\w\\8b-\\8a*4r-]|\\\\\\\\.)",6v=1B 4q("^>\\\\s*("+C+"+)"),6u=1B 4q("^("+C+"+)(#)("+C+"+)"),6s=1B 4q("^([#.]?)("+C+"*)");E.1s({6r:{"":J(a,i,m){K m[2]=="*"||E.12(a,m[2])},"#":J(a,i,m){K a.4z("2w")==m[2]},":":{89:J(a,i,m){K i<m[3]-0},88:J(a,i,m){K i>m[3]-0},2Z:J(a,i,m){K m[3]-0==i},6Z:J(a,i,m){K m[3]-0==i},3j:J(a,i){K i==0},3J:J(a,i,m,r){K i==r.M-1},6n:J(a,i){K i%2==0},6l:J(a,i){K i%2},"3j-4p":J(a){K a.1a.3S("*")[0]==a},"3J-4p":J(a){K E.2Z(a.1a.5o,1,"4t")==a},"83-4p":J(a){K!E.2Z(a.1a.5o,2,"4t")},6B:J(a){K a.1C},4x:J(a){K!a.1C},82:J(a,i,m){K(a.6x||a.81||E(a).1u()||"").1f(m[3])>=0},4d:J(a){K"1Z"!=a.U&&E.1j(a,"19")!="2H"&&E.1j(a,"4U")!="1Z"},1Z:J(a){K"1Z"==a.U||E.1j(a,"19")=="2H"||E.1j(a,"4U")=="1Z"},80:J(a){K!a.2Y},2Y:J(a){K a.2Y},3k:J(a){K a.3k},2p:J(a){K a.2p||E.1J(a,"2p")},1u:J(a){K"1u"==a.U},5u:J(a){K"5u"==a.U},5t:J(a){K"5t"==a.U},59:J(a){K"59"==a.U},3I:J(a){K"3I"==a.U},58:J(a){K"58"==a.U},6j:J(a){K"6j"==a.U},6i:J(a){K"6i"==a.U},2G:J(a){K"2G"==a.U||E.12(a,"2G")},4D:J(a){K/4D|2k|6h|2G/i.17(a.12)},3Y:J(a,i,m){K E.2s(m[3],a).M},7X:J(a){K/h\\d/i.17(a.12)},7W:J(a){K E.3y(E.3G,J(b){K a==b.Y}).M}}},6g:[/^(\\[) *@?([\\w-]+) *([!*$^~=]*) *(\'?"?)(.*?)\\4 *\\]/,/^(:)([\\w-]+)\\("?\'?(.*?(\\(.*?\\))?[^(]*?)"?\'?\\)/,1B 4q("^([:.#]*)("+C+"+)")],3e:J(a,c,b){L d,2m=[];2b(a&&a!=d){d=a;L f=E.1E(a,c,b);a=f.t.1r(/^\\s*,\\s*/,"");2m=b?c=f.r:E.37(2m,f.r)}K 2m},2s:J(t,p){7(1o t!="25")K[t];7(p&&p.15!=1&&p.15!=9)K[];p=p||T;L d=[p],2r=[],3J,12;2b(t&&3J!=t){L r=[];3J=t;t=E.3g(t);L o=S;L g=6v;L m=g.2O(t);7(m){12=m[1].2E();Q(L i=0;d[i];i++)Q(L c=d[i].1C;c;c=c.2B)7(c.15==1&&(12=="*"||c.12.2E()==12))r.1g(c);d=r;t=t.1r(g,"");7(t.1f(" ")==0)6w;o=P}N{g=/^([>+~])\\s*(\\w*)/i;7((m=g.2O(t))!=V){r=[];L l={};12=m[2].2E();m=m[1];Q(L j=0,3f=d.M;j<3f;j++){L n=m=="~"||m=="+"?d[j].2B:d[j].1C;Q(;n;n=n.2B)7(n.15==1){L h=E.O(n);7(m=="~"&&l[h])1Q;7(!12||n.12.2E()==12){7(m=="~")l[h]=P;r.1g(n)}7(m=="+")1Q}}d=r;t=E.3g(t.1r(g,""));o=P}}7(t&&!o){7(!t.1f(",")){7(p==d[0])d.4l();2r=E.37(2r,d);r=d=[p];t=" "+t.6e(1,t.M)}N{L k=6u;L m=k.2O(t);7(m){m=[0,m[2],m[3],m[1]]}N{k=6s;m=k.2O(t)}m[2]=m[2].1r(/\\\\/g,"");L f=d[d.M-1];7(m[1]=="#"&&f&&f.5J&&!E.3E(f)){L q=f.5J(m[2]);7((E.14.1d||E.14.2z)&&q&&1o q.2w=="25"&&q.2w!=m[2])q=E(\'[@2w="\'+m[2]+\'"]\',f)[0];d=r=q&&(!m[3]||E.12(q,m[3]))?[q]:[]}N{Q(L i=0;d[i];i++){L a=m[1]=="#"&&m[3]?m[3]:m[1]!=""||m[0]==""?"*":m[2];7(a=="*"&&d[i].12.2h()=="3V")a="3m";r=E.37(r,d[i].3S(a))}7(m[1]==".")r=E.55(r,m[2]);7(m[1]=="#"){L e=[];Q(L i=0;r[i];i++)7(r[i].4z("2w")==m[2]){e=[r[i]];1Q}r=e}d=r}t=t.1r(k,"")}}7(t){L b=E.1E(t,r);d=r=b.r;t=E.3g(b.t)}}7(t)d=[];7(d&&p==d[0])d.4l();2r=E.37(2r,d);K 2r},55:J(r,m,a){m=" "+m+" ";L c=[];Q(L i=0;r[i];i++){L b=(" "+r[i].1t+" ").1f(m)>=0;7(!a&&b||a&&!b)c.1g(r[i])}K c},1E:J(t,r,h){L d;2b(t&&t!=d){d=t;L p=E.6g,m;Q(L i=0;p[i];i++){m=p[i].2O(t);7(m){t=t.7V(m[0].M);m[2]=m[2].1r(/\\\\/g,"");1Q}}7(!m)1Q;7(m[1]==":"&&m[2]=="56")r=G.17(m[3])?E.1E(m[3],r,P).r:E(r).56(m[3]);N 7(m[1]==".")r=E.55(r,m[2],h);N 7(m[1]=="["){L g=[],U=m[3];Q(L i=0,3f=r.M;i<3f;i++){L a=r[i],z=a[E.46[m[2]]||m[2]];7(z==V||/6O|3Q|2p/.17(m[2]))z=E.1J(a,m[2])||\'\';7((U==""&&!!z||U=="="&&z==m[5]||U=="!="&&z!=m[5]||U=="^="&&z&&!z.1f(m[5])||U=="$="&&z.6e(z.M-m[5].M)==m[5]||(U=="*="||U=="~=")&&z.1f(m[5])>=0)^h)g.1g(a)}r=g}N 7(m[1]==":"&&m[2]=="2Z-4p"){L e={},g=[],17=/(-?)(\\d*)n((?:\\+|-)?\\d*)/.2O(m[3]=="6n"&&"2n"||m[3]=="6l"&&"2n+1"||!/\\D/.17(m[3])&&"7U+"+m[3]||m[3]),3j=(17[1]+(17[2]||1))-0,d=17[3]-0;Q(L i=0,3f=r.M;i<3f;i++){L j=r[i],1a=j.1a,2w=E.O(1a);7(!e[2w]){L c=1;Q(L n=1a.1C;n;n=n.2B)7(n.15==1)n.4k=c++;e[2w]=P}L b=S;7(3j==0){7(j.4k==d)b=P}N 7((j.4k-d)%3j==0&&(j.4k-d)/3j>=0)b=P;7(b^h)g.1g(j)}r=g}N{L f=E.6r[m[1]];7(1o f=="3V")f=f[m[2]];7(1o f=="25")f=6c("S||J(a,i){K "+f+";}");r=E.3y(r,J(a,i){K f(a,i,m,r)},h)}}K{r:r,t:t}},4u:J(b,c){L d=[];L a=b[c];2b(a&&a!=T){7(a.15==1)d.1g(a);a=a[c]}K d},2Z:J(a,e,c,b){e=e||1;L d=0;Q(;a;a=a[c])7(a.15==1&&++d==e)1Q;K a},5i:J(n,a){L r=[];Q(;n;n=n.2B){7(n.15==1&&(!a||n!=a))r.1g(n)}K r}});E.16={1b:J(f,i,g,e){7(f.15==3||f.15==8)K;7(E.14.1d&&f.53!=10)f=1e;7(!g.2D)g.2D=6.2D++;7(e!=10){L h=g;g=J(){K h.1i(6,18)};g.O=e;g.2D=h.2D}L j=E.O(f,"2R")||E.O(f,"2R",{}),1v=E.O(f,"1v")||E.O(f,"1v",J(){L a;7(1o E=="10"||E.16.5f)K a;a=E.16.1v.1i(18.3R.Y,18);K a});1v.Y=f;E.R(i.23(/\\s+/),J(c,b){L a=b.23(".");b=a[0];g.U=a[1];L d=j[b];7(!d){d=j[b]={};7(!E.16.2y[b]||E.16.2y[b].4j.1P(f)===S){7(f.3F)f.3F(b,1v,S);N 7(f.6b)f.6b("4i"+b,1v)}}d[g.2D]=g;E.16.2a[b]=P});f=V},2D:1,2a:{},1V:J(e,h,f){7(e.15==3||e.15==8)K;L i=E.O(e,"2R"),29,4X;7(i){7(h==10||(1o h=="25"&&h.7T(0)=="."))Q(L g 1p i)6.1V(e,g+(h||""));N{7(h.U){f=h.2q;h=h.U}E.R(h.23(/\\s+/),J(b,a){L c=a.23(".");a=c[0];7(i[a]){7(f)2V i[a][f.2D];N Q(f 1p i[a])7(!c[1]||i[a][f].U==c[1])2V i[a][f];Q(29 1p i[a])1Q;7(!29){7(!E.16.2y[a]||E.16.2y[a].4h.1P(e)===S){7(e.67)e.67(a,E.O(e,"1v"),S);N 7(e.66)e.66("4i"+a,E.O(e,"1v"))}29=V;2V i[a]}}})}Q(29 1p i)1Q;7(!29){L d=E.O(e,"1v");7(d)d.Y=V;E.35(e,"2R");E.35(e,"1v")}}},1N:J(g,c,d,f,h){c=E.2I(c||[]);7(g.1f("!")>=0){g=g.2K(0,-1);L a=P}7(!d){7(6.2a[g])E("*").1b([1e,T]).1N(g,c)}N{7(d.15==3||d.15==8)K 10;L b,29,1n=E.1q(d[g]||V),16=!c[0]||!c[0].36;7(16)c.4J(6.4Z({U:g,2L:d}));c[0].U=g;7(a)c[0].65=P;7(E.1q(E.O(d,"1v")))b=E.O(d,"1v").1i(d,c);7(!1n&&d["4i"+g]&&d["4i"+g].1i(d,c)===S)b=S;7(16)c.4l();7(h&&E.1q(h)){29=h.1i(d,b==V?c:c.71(b));7(29!==10)b=29}7(1n&&f!==S&&b!==S&&!(E.12(d,\'a\')&&g=="4V")){6.5f=P;1S{d[g]()}1X(e){}}6.5f=S}K b},1v:J(c){L a;c=E.16.4Z(c||1e.16||{});L b=c.U.23(".");c.U=b[0];L f=E.O(6,"2R")&&E.O(6,"2R")[c.U],42=1M.2l.2K.1P(18,1);42.4J(c);Q(L j 1p f){L d=f[j];42[0].2q=d;42[0].O=d.O;7(!b[1]&&!c.65||d.U==b[1]){L e=d.1i(6,42);7(a!==S)a=e;7(e===S){c.36();c.44()}}}7(E.14.1d)c.2L=c.36=c.44=c.2q=c.O=V;K a},4Z:J(c){L a=c;c=E.1s({},a);c.36=J(){7(a.36)a.36();a.7S=S};c.44=J(){7(a.44)a.44();a.7R=P};7(!c.2L)c.2L=c.7Q||T;7(c.2L.15==3)c.2L=a.2L.1a;7(!c.4S&&c.5w)c.4S=c.5w==c.2L?c.7P:c.5w;7(c.64==V&&c.63!=V){L b=T.1F,1h=T.1h;c.64=c.63+(b&&b.2v||1h&&1h.2v||0)-(b.62||0);c.7N=c.7L+(b&&b.2x||1h&&1h.2x||0)-(b.60||0)}7(!c.3c&&((c.4f||c.4f===0)?c.4f:c.5Z))c.3c=c.4f||c.5Z;7(!c.7b&&c.5Y)c.7b=c.5Y;7(!c.3c&&c.2G)c.3c=(c.2G&1?1:(c.2G&2?3:(c.2G&4?2:0)));K c},2y:{21:{4j:J(){5M();K},4h:J(){K}},3C:{4j:J(){7(E.14.1d)K S;E(6).2j("4P",E.16.2y.3C.2q);K P},4h:J(){7(E.14.1d)K S;E(6).3w("4P",E.16.2y.3C.2q);K P},2q:J(a){7(I(a,6))K P;18[0].U="3C";K E.16.1v.1i(6,18)}},3B:{4j:J(){7(E.14.1d)K S;E(6).2j("4O",E.16.2y.3B.2q);K P},4h:J(){7(E.14.1d)K S;E(6).3w("4O",E.16.2y.3B.2q);K P},2q:J(a){7(I(a,6))K P;18[0].U="3B";K E.16.1v.1i(6,18)}}}};E.1n.1s({2j:J(c,a,b){K c=="4H"?6.2X(c,a,b):6.R(J(){E.16.1b(6,c,b||a,b&&a)})},2X:J(d,b,c){K 6.R(J(){E.16.1b(6,d,J(a){E(6).3w(a);K(c||b).1i(6,18)},c&&b)})},3w:J(a,b){K 6.R(J(){E.16.1V(6,a,b)})},1N:J(c,a,b){K 6.R(J(){E.16.1N(c,a,6,P,b)})},5n:J(c,a,b){7(6[0])K E.16.1N(c,a,6[0],S,b);K 10},2g:J(){L b=18;K 6.4V(J(a){6.4N=0==6.4N?1:0;a.36();K b[6.4N].1i(6,18)||S})},7D:J(a,b){K 6.2j(\'3C\',a).2j(\'3B\',b)},21:J(a){5M();7(E.2Q)a.1P(T,E);N E.3A.1g(J(){K a.1P(6,E)});K 6}});E.1s({2Q:S,3A:[],21:J(){7(!E.2Q){E.2Q=P;7(E.3A){E.R(E.3A,J(){6.1i(T)});E.3A=V}E(T).5n("21")}}});L x=S;J 5M(){7(x)K;x=P;7(T.3F&&!E.14.2z)T.3F("5W",E.21,S);7(E.14.1d&&1e==3b)(J(){7(E.2Q)K;1S{T.1F.7B("26")}1X(3a){3z(18.3R,0);K}E.21()})();7(E.14.2z)T.3F("5W",J(){7(E.2Q)K;Q(L i=0;i<T.4L.M;i++)7(T.4L[i].2Y){3z(18.3R,0);K}E.21()},S);7(E.14.2d){L a;(J(){7(E.2Q)K;7(T.39!="5V"&&T.39!="1y"){3z(18.3R,0);K}7(a===10)a=E("W, 7a[7A=7z]").M;7(T.4L.M!=a){3z(18.3R,0);K}E.21()})()}E.16.1b(1e,"3U",E.21)}E.R(("7y,7x,3U,7w,5d,4H,4V,7v,"+"7G,7u,7t,4P,4O,7s,2k,"+"58,7K,7q,7p,3a").23(","),J(i,b){E.1n[b]=J(a){K a?6.2j(b,a):6.1N(b)}});L I=J(a,c){L b=a.4S;2b(b&&b!=c)1S{b=b.1a}1X(3a){b=c}K b==c};E(1e).2j("4H",J(){E("*").1b(T).3w()});E.1n.1s({3U:J(g,d,c){7(E.1q(g))K 6.2j("3U",g);L e=g.1f(" ");7(e>=0){L i=g.2K(e,g.M);g=g.2K(0,e)}c=c||J(){};L f="4Q";7(d)7(E.1q(d)){c=d;d=V}N{d=E.3m(d);f="61"}L h=6;E.3P({1c:g,U:f,1H:"3q",O:d,1y:J(a,b){7(b=="1W"||b=="5U")h.3q(i?E("<1x/>").3t(a.4b.1r(/<1m(.|\\s)*?\\/1m>/g,"")).2s(i):a.4b);h.R(c,[a.4b,b,a])}});K 6},7n:J(){K E.3m(6.5T())},5T:J(){K 6.2c(J(){K E.12(6,"3u")?E.2I(6.7m):6}).1E(J(){K 6.31&&!6.2Y&&(6.3k||/2k|6h/i.17(6.12)||/1u|1Z|3I/i.17(6.U))}).2c(J(i,c){L b=E(6).5O();K b==V?V:b.1k==1M?E.2c(b,J(a,i){K{31:c.31,1A:a}}):{31:c.31,1A:b}}).22()}});E.R("5S,6d,5R,6D,5Q,6m".23(","),J(i,o){E.1n[o]=J(f){K 6.2j(o,f)}});L B=(1B 3v).3L();E.1s({22:J(d,b,a,c){7(E.1q(b)){a=b;b=V}K E.3P({U:"4Q",1c:d,O:b,1W:a,1H:c})},7l:J(b,a){K E.22(b,V,a,"1m")},7k:J(c,b,a){K E.22(c,b,a,"3i")},7i:J(d,b,a,c){7(E.1q(b)){a=b;b={}}K E.3P({U:"61",1c:d,O:b,1W:a,1H:c})},85:J(a){E.1s(E.4I,a)},4I:{2a:P,U:"4Q",2U:0,5P:"4o/x-7h-3u-7g",5N:P,3l:P,O:V,6p:V,3I:V,49:{3M:"4o/3M, 1u/3M",3q:"1u/3q",1m:"1u/4m, 4o/4m",3i:"4o/3i, 1u/4m",1u:"1u/a7",4G:"*/*"}},4F:{},3P:J(s){L f,2W=/=\\?(&|$)/g,1z,O;s=E.1s(P,s,E.1s(P,{},E.4I,s));7(s.O&&s.5N&&1o s.O!="25")s.O=E.3m(s.O);7(s.1H=="4E"){7(s.U.2h()=="22"){7(!s.1c.1D(2W))s.1c+=(s.1c.1D(/\\?/)?"&":"?")+(s.4E||"7d")+"=?"}N 7(!s.O||!s.O.1D(2W))s.O=(s.O?s.O+"&":"")+(s.4E||"7d")+"=?";s.1H="3i"}7(s.1H=="3i"&&(s.O&&s.O.1D(2W)||s.1c.1D(2W))){f="4E"+B++;7(s.O)s.O=(s.O+"").1r(2W,"="+f+"$1");s.1c=s.1c.1r(2W,"="+f+"$1");s.1H="1m";1e[f]=J(a){O=a;1W();1y();1e[f]=10;1S{2V 1e[f]}1X(e){}7(h)h.34(g)}}7(s.1H=="1m"&&s.1T==V)s.1T=S;7(s.1T===S&&s.U.2h()=="22"){L i=(1B 3v()).3L();L j=s.1c.1r(/(\\?|&)4r=.*?(&|$)/,"$a4="+i+"$2");s.1c=j+((j==s.1c)?(s.1c.1D(/\\?/)?"&":"?")+"4r="+i:"")}7(s.O&&s.U.2h()=="22"){s.1c+=(s.1c.1D(/\\?/)?"&":"?")+s.O;s.O=V}7(s.2a&&!E.5H++)E.16.1N("5S");7((!s.1c.1f("a3")||!s.1c.1f("//"))&&s.1H=="1m"&&s.U.2h()=="22"){L h=T.3S("6f")[0];L g=T.3s("1m");g.3Q=s.1c;7(s.7c)g.a2=s.7c;7(!f){L l=S;g.9Z=g.9Y=J(){7(!l&&(!6.39||6.39=="5V"||6.39=="1y")){l=P;1W();1y();h.34(g)}}}h.38(g);K 10}L m=S;L k=1e.78?1B 78("9X.9V"):1B 76();k.9T(s.U,s.1c,s.3l,s.6p,s.3I);1S{7(s.O)k.4C("9R-9Q",s.5P);7(s.5C)k.4C("9O-5A-9N",E.4F[s.1c]||"9L, 9K 9I 9H 5z:5z:5z 9F");k.4C("X-9C-9A","76");k.4C("9z",s.1H&&s.49[s.1H]?s.49[s.1H]+", */*":s.49.4G)}1X(e){}7(s.6Y)s.6Y(k);7(s.2a)E.16.1N("6m",[k,s]);L c=J(a){7(!m&&k&&(k.39==4||a=="2U")){m=P;7(d){6I(d);d=V}1z=a=="2U"&&"2U"||!E.6X(k)&&"3a"||s.5C&&E.6J(k,s.1c)&&"5U"||"1W";7(1z=="1W"){1S{O=E.6W(k,s.1H)}1X(e){1z="5x"}}7(1z=="1W"){L b;1S{b=k.5q("6U-5A")}1X(e){}7(s.5C&&b)E.4F[s.1c]=b;7(!f)1W()}N E.5v(s,k,1z);1y();7(s.3l)k=V}};7(s.3l){L d=53(c,13);7(s.2U>0)3z(J(){7(k){k.9t();7(!m)c("2U")}},s.2U)}1S{k.9s(s.O)}1X(e){E.5v(s,k,V,e)}7(!s.3l)c();J 1W(){7(s.1W)s.1W(O,1z);7(s.2a)E.16.1N("5Q",[k,s])}J 1y(){7(s.1y)s.1y(k,1z);7(s.2a)E.16.1N("5R",[k,s]);7(s.2a&&!--E.5H)E.16.1N("6d")}K k},5v:J(s,a,b,e){7(s.3a)s.3a(a,b,e);7(s.2a)E.16.1N("6D",[a,s,e])},5H:0,6X:J(r){1S{K!r.1z&&9q.9p=="59:"||(r.1z>=6T&&r.1z<9n)||r.1z==6R||r.1z==9l||E.14.2d&&r.1z==10}1X(e){}K S},6J:J(a,c){1S{L b=a.5q("6U-5A");K a.1z==6R||b==E.4F[c]||E.14.2d&&a.1z==10}1X(e){}K S},6W:J(r,b){L c=r.5q("9k-U");L d=b=="3M"||!b&&c&&c.1f("3M")>=0;L a=d?r.9j:r.4b;7(d&&a.1F.28=="5x")6Q"5x";7(b=="1m")E.5g(a);7(b=="3i")a=6c("("+a+")");K a},3m:J(a){L s=[];7(a.1k==1M||a.5h)E.R(a,J(){s.1g(3r(6.31)+"="+3r(6.1A))});N Q(L j 1p a)7(a[j]&&a[j].1k==1M)E.R(a[j],J(){s.1g(3r(j)+"="+3r(6))});N s.1g(3r(j)+"="+3r(a[j]));K s.6a("&").1r(/%20/g,"+")}});E.1n.1s({1G:J(c,b){K c?6.2e({1R:"1G",27:"1G",1w:"1G"},c,b):6.1E(":1Z").R(J(){6.W.19=6.5s||"";7(E.1j(6,"19")=="2H"){L a=E("<"+6.28+" />").6y("1h");6.W.19=a.1j("19");7(6.W.19=="2H")6.W.19="3D";a.1V()}}).3h()},1I:J(b,a){K b?6.2e({1R:"1I",27:"1I",1w:"1I"},b,a):6.1E(":4d").R(J(){6.5s=6.5s||E.1j(6,"19");6.W.19="2H"}).3h()},6N:E.1n.2g,2g:J(a,b){K E.1q(a)&&E.1q(b)?6.6N(a,b):a?6.2e({1R:"2g",27:"2g",1w:"2g"},a,b):6.R(J(){E(6)[E(6).3H(":1Z")?"1G":"1I"]()})},9f:J(b,a){K 6.2e({1R:"1G"},b,a)},9d:J(b,a){K 6.2e({1R:"1I"},b,a)},9c:J(b,a){K 6.2e({1R:"2g"},b,a)},9a:J(b,a){K 6.2e({1w:"1G"},b,a)},99:J(b,a){K 6.2e({1w:"1I"},b,a)},97:J(c,a,b){K 6.2e({1w:a},c,b)},2e:J(l,k,j,h){L i=E.6P(k,j,h);K 6[i.2P===S?"R":"2P"](J(){7(6.15!=1)K S;L g=E.1s({},i);L f=E(6).3H(":1Z"),4A=6;Q(L p 1p l){7(l[p]=="1I"&&f||l[p]=="1G"&&!f)K E.1q(g.1y)&&g.1y.1i(6);7(p=="1R"||p=="27"){g.19=E.1j(6,"19");g.32=6.W.32}}7(g.32!=V)6.W.32="1Z";g.40=E.1s({},l);E.R(l,J(c,a){L e=1B E.2t(4A,g,c);7(/2g|1G|1I/.17(a))e[a=="2g"?f?"1G":"1I":a](l);N{L b=a.3X().1D(/^([+-]=)?([\\d+-.]+)(.*)$/),1Y=e.2m(P)||0;7(b){L d=2M(b[2]),2A=b[3]||"2S";7(2A!="2S"){4A.W[c]=(d||1)+2A;1Y=((d||1)/e.2m(P))*1Y;4A.W[c]=1Y+2A}7(b[1])d=((b[1]=="-="?-1:1)*d)+1Y;e.45(1Y,d,2A)}N e.45(1Y,a,"")}});K P})},2P:J(a,b){7(E.1q(a)||(a&&a.1k==1M)){b=a;a="2t"}7(!a||(1o a=="25"&&!b))K A(6[0],a);K 6.R(J(){7(b.1k==1M)A(6,a,b);N{A(6,a).1g(b);7(A(6,a).M==1)b.1i(6)}})},94:J(b,c){L a=E.3G;7(b)6.2P([]);6.R(J(){Q(L i=a.M-1;i>=0;i--)7(a[i].Y==6){7(c)a[i](P);a.72(i,1)}});7(!c)6.5p();K 6}});L A=J(b,c,a){7(!b)K 10;c=c||"2t";L q=E.O(b,c+"2P");7(!q||a)q=E.O(b,c+"2P",a?E.2I(a):[]);K q};E.1n.5p=J(a){a=a||"2t";K 6.R(J(){L q=A(6,a);q.4l();7(q.M)q[0].1i(6)})};E.1s({6P:J(b,a,c){L d=b&&b.1k==92?b:{1y:c||!c&&a||E.1q(b)&&b,2u:b,3Z:c&&a||a&&a.1k!=91&&a};d.2u=(d.2u&&d.2u.1k==51?d.2u:{90:8Z,9D:6T}[d.2u])||8X;d.5y=d.1y;d.1y=J(){7(d.2P!==S)E(6).5p();7(E.1q(d.5y))d.5y.1i(6)};K d},3Z:{70:J(p,n,b,a){K b+a*p},5j:J(p,n,b,a){K((-24.8V(p*24.8U)/2)+0.5)*a+b}},3G:[],3W:V,2t:J(b,c,a){6.11=c;6.Y=b;6.1l=a;7(!c.47)c.47={}}});E.2t.2l={4y:J(){7(6.11.30)6.11.30.1i(6.Y,[6.2J,6]);(E.2t.30[6.1l]||E.2t.30.4G)(6);7(6.1l=="1R"||6.1l=="27")6.Y.W.19="3D"},2m:J(a){7(6.Y[6.1l]!=V&&6.Y.W[6.1l]==V)K 6.Y[6.1l];L r=2M(E.1j(6.Y,6.1l,a));K r&&r>-8Q?r:2M(E.2o(6.Y,6.1l))||0},45:J(c,b,d){6.5B=(1B 3v()).3L();6.1Y=c;6.3h=b;6.2A=d||6.2A||"2S";6.2J=6.1Y;6.4B=6.4w=0;6.4y();L e=6;J t(a){K e.30(a)}t.Y=6.Y;E.3G.1g(t);7(E.3W==V){E.3W=53(J(){L a=E.3G;Q(L i=0;i<a.M;i++)7(!a[i]())a.72(i--,1);7(!a.M){6I(E.3W);E.3W=V}},13)}},1G:J(){6.11.47[6.1l]=E.1J(6.Y.W,6.1l);6.11.1G=P;6.45(0,6.2m());7(6.1l=="27"||6.1l=="1R")6.Y.W[6.1l]="8N";E(6.Y).1G()},1I:J(){6.11.47[6.1l]=E.1J(6.Y.W,6.1l);6.11.1I=P;6.45(6.2m(),0)},30:J(a){L t=(1B 3v()).3L();7(a||t>6.11.2u+6.5B){6.2J=6.3h;6.4B=6.4w=1;6.4y();6.11.40[6.1l]=P;L b=P;Q(L i 1p 6.11.40)7(6.11.40[i]!==P)b=S;7(b){7(6.11.19!=V){6.Y.W.32=6.11.32;6.Y.W.19=6.11.19;7(E.1j(6.Y,"19")=="2H")6.Y.W.19="3D"}7(6.11.1I)6.Y.W.19="2H";7(6.11.1I||6.11.1G)Q(L p 1p 6.11.40)E.1J(6.Y.W,p,6.11.47[p])}7(b&&E.1q(6.11.1y))6.11.1y.1i(6.Y);K S}N{L n=t-6.5B;6.4w=n/6.11.2u;6.4B=E.3Z[6.11.3Z||(E.3Z.5j?"5j":"70")](6.4w,n,0,1,6.11.2u);6.2J=6.1Y+((6.3h-6.1Y)*6.4B);6.4y()}K P}};E.2t.30={2v:J(a){a.Y.2v=a.2J},2x:J(a){a.Y.2x=a.2J},1w:J(a){E.1J(a.Y.W,"1w",a.2J)},4G:J(a){a.Y.W[a.1l]=a.2J+a.2A}};E.1n.5L=J(){L b=0,3b=0,Y=6[0],5l;7(Y)8M(E.14){L d=Y.1a,41=Y,1K=Y.1K,1L=Y.2i,5D=2d&&4s(5K)<8J&&!/a1/i.17(v),2T=E.1j(Y,"43")=="2T";7(Y.6G){L c=Y.6G();1b(c.26+24.2f(1L.1F.2v,1L.1h.2v),c.3b+24.2f(1L.1F.2x,1L.1h.2x));1b(-1L.1F.62,-1L.1F.60)}N{1b(Y.5G,Y.5F);2b(1K){1b(1K.5G,1K.5F);7(48&&!/^t(8H|d|h)$/i.17(1K.28)||2d&&!5D)2N(1K);7(!2T&&E.1j(1K,"43")=="2T")2T=P;41=/^1h$/i.17(1K.28)?41:1K;1K=1K.1K}2b(d&&d.28&&!/^1h|3q$/i.17(d.28)){7(!/^8G|1O.*$/i.17(E.1j(d,"19")))1b(-d.2v,-d.2x);7(48&&E.1j(d,"32")!="4d")2N(d);d=d.1a}7((5D&&(2T||E.1j(41,"43")=="4W"))||(48&&E.1j(41,"43")!="4W"))1b(-1L.1h.5G,-1L.1h.5F);7(2T)1b(24.2f(1L.1F.2v,1L.1h.2v),24.2f(1L.1F.2x,1L.1h.2x))}5l={3b:3b,26:b}}J 2N(a){1b(E.2o(a,"a8",P),E.2o(a,"a9",P))}J 1b(l,t){b+=4s(l)||0;3b+=4s(t)||0}K 5l}})();',62,631,'||||||this|if||||||||||||||||||||||||||||||||||||||function|return|var|length|else|data|true|for|each|false|document|type|null|style||elem||undefined|options|nodeName||browser|nodeType|event|test|arguments|display|parentNode|add|url|msie|window|indexOf|push|body|apply|css|constructor|prop|script|fn|typeof|in|isFunction|replace|extend|className|text|handle|opacity|div|complete|status|value|new|firstChild|match|filter|documentElement|show|dataType|hide|attr|offsetParent|doc|Array|trigger|table|call|break|height|try|cache|tbody|remove|success|catch|start|hidden||ready|get|split|Math|string|left|width|tagName|ret|global|while|map|safari|animate|max|toggle|toLowerCase|ownerDocument|bind|select|prototype|cur||curCSS|selected|handler|done|find|fx|duration|scrollLeft|id|scrollTop|special|opera|unit|nextSibling|stack|guid|toUpperCase|pushStack|button|none|makeArray|now|slice|target|parseFloat|border|exec|queue|isReady|events|px|fixed|timeout|delete|jsre|one|disabled|nth|step|name|overflow|inArray|removeChild|removeData|preventDefault|merge|appendChild|readyState|error|top|which|innerHTML|multiFilter|rl|trim|end|json|first|checked|async|param|elems|insertBefore|childNodes|html|encodeURIComponent|createElement|append|form|Date|unbind|color|grep|setTimeout|readyList|mouseleave|mouseenter|block|isXMLDoc|addEventListener|timers|is|password|last|runtimeStyle|getTime|xml|jQuery|domManip|ajax|src|callee|getElementsByTagName|selectedIndex|load|object|timerId|toString|has|easing|curAnim|offsetChild|args|position|stopPropagation|custom|props|orig|mozilla|accepts|clean|responseText|defaultView|visible|String|charCode|float|teardown|on|setup|nodeIndex|shift|javascript|currentStyle|application|child|RegExp|_|parseInt|previousSibling|dir|tr|state|empty|update|getAttribute|self|pos|setRequestHeader|input|jsonp|lastModified|_default|unload|ajaxSettings|unshift|getComputedStyle|styleSheets|getPropertyValue|lastToggle|mouseout|mouseover|GET|andSelf|relatedTarget|init|visibility|click|absolute|index|container|fix|outline|Number|removeAttribute|setInterval|prevObject|classFilter|not|unique|submit|file|after|windowData|deep|scroll|client|triggered|globalEval|jquery|sibling|swing|clone|results|wrapAll|triggerHandler|lastChild|dequeue|getResponseHeader|createTextNode|oldblock|checkbox|radio|handleError|fromElement|parsererror|old|00|Modified|startTime|ifModified|safari2|getWH|offsetTop|offsetLeft|active|values|getElementById|version|offset|bindReady|processData|val|contentType|ajaxSuccess|ajaxComplete|ajaxStart|serializeArray|notmodified|loaded|DOMContentLoaded|Width|ctrlKey|keyCode|clientTop|POST|clientLeft|clientX|pageX|exclusive|detachEvent|removeEventListener|swap|cloneNode|join|attachEvent|eval|ajaxStop|substr|head|parse|textarea|reset|image|zoom|odd|ajaxSend|even|before|username|prepend|expr|quickClass|uuid|quickID|quickChild|continue|textContent|appendTo|contents|evalScript|parent|defaultValue|ajaxError|setArray|compatMode|getBoundingClientRect|styleFloat|clearInterval|httpNotModified|nodeValue|100|alpha|_toggle|href|speed|throw|304|replaceWith|200|Last|colgroup|httpData|httpSuccess|beforeSend|eq|linear|concat|splice|fieldset|multiple|cssFloat|XMLHttpRequest|webkit|ActiveXObject|CSS1Compat|link|metaKey|scriptCharset|callback|col|pixelLeft|urlencoded|www|post|hasClass|getJSON|getScript|elements|serialize|black|keyup|keypress|solid|change|mousemove|mouseup|dblclick|resize|focus|blur|stylesheet|rel|doScroll|round|hover|padding|offsetHeight|mousedown|offsetWidth|Bottom|Top|keydown|clientY|Right|pageY|Left|toElement|srcElement|cancelBubble|returnValue|charAt|0n|substring|animated|header|noConflict|line|enabled|innerText|contains|only|weight|ajaxSetup|font|size|gt|lt|uFFFF|u0128|417|Boolean|inner|Height|toggleClass|removeClass|addClass|removeAttr|replaceAll|insertAfter|prependTo|contentWindow|contentDocument|wrap|iframe|children|siblings|prevAll|nextAll|prev|wrapInner|next|parents|maxLength|maxlength|readOnly|readonly|reverse|class|htmlFor|inline|able|boxModel|522|setData|compatible|with|1px|ie|getData|10000|ra|it|rv|PI|cos|userAgent|400|navigator|600|slow|Function|Object|array|stop|ig|NaN|fadeTo|option|fadeOut|fadeIn|setAttribute|slideToggle|slideUp|changed|slideDown|be|can|property|responseXML|content|1223|getAttributeNode|300|method|protocol|location|action|send|abort|cssText|th|td|cap|specified|Accept|With|colg|Requested|fast|tfoot|GMT|thead|1970|Jan|attributes|01|Thu|leg|Since|If|opt|Type|Content|embed|open|area|XMLHTTP|hr|Microsoft|onreadystatechange|onload|meta|adobeair|charset|http|1_|img|br|plain|borderLeftWidth|borderTopWidth|abbr'.split('|'),0,{}))
\ No newline at end of file
/**
* jQuery.ScrollTo - Easy element scrolling using jQuery.
* Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
* Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
* and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
* Date: 2/19/2008
* @author Ariel Flesler
* @version 1.3.3
*/
;(function($){var o=$.scrollTo=function(a,b,c){o.window().scrollTo(a,b,c)};o.defaults={axis:'y',duration:1};o.window=function(){return $($.browser.safari?'body':'html')};$.fn.scrollTo=function(l,m,n){if(typeof m=='object'){n=m;m=0}n=$.extend({},o.defaults,n);m=m||n.speed||n.duration;n.queue=n.queue&&n.axis.length>1;if(n.queue)m/=2;n.offset=j(n.offset);n.over=j(n.over);return this.each(function(){var a=this,b=$(a),t=l,c,d={},w=b.is('html,body');switch(typeof t){case'number':case'string':if(/^([+-]=)?\d+(px)?$/.test(t)){t=j(t);break}t=$(t,this);case'object':if(t.is||t.style)c=(t=$(t)).offset()}$.each(n.axis.split(''),function(i,f){var P=f=='x'?'Left':'Top',p=P.toLowerCase(),k='scroll'+P,e=a[k],D=f=='x'?'Width':'Height';if(c){d[k]=c[p]+(w?0:e-b.offset()[p]);if(n.margin){d[k]-=parseInt(t.css('margin'+P))||0;d[k]-=parseInt(t.css('border'+P+'Width'))||0}d[k]+=n.offset[p]||0;if(n.over[p])d[k]+=t[D.toLowerCase()]()*n.over[p]}else d[k]=t[p];if(/^\d+$/.test(d[k]))d[k]=d[k]<=0?0:Math.min(d[k],h(D));if(!i&&n.queue){if(e!=d[k])g(n.onAfterFirst);delete d[k]}});g(n.onAfter);function g(a){b.animate(d,m,n.easing,a&&function(){a.call(this,l)})};function h(D){var b=w?$.browser.opera?document.body:document.documentElement:a;return b['scroll'+D]-b['client'+D]}})};function j(a){return typeof a=='object'?a:{top:a,left:a}}})(jQuery);
\ No newline at end of file
/*
*
* Copyright (c) 2006/2007 Sam Collett (http://www.texotela.co.uk)
* Licensed under the MIT License:
* http://www.opensource.org/licenses/mit-license.php
*
* Version 2.2.1
* Demo: http://www.texotela.co.uk/code/jquery/select/
*
* $LastChangedDate$
* $Rev$
*
*/
(function($) {
/**
* Adds (single/multiple) options to a select box (or series of select boxes)
*
* @name addOption
* @author Sam Collett (http://www.texotela.co.uk)
* @type jQuery
* @example $("#myselect").addOption("Value", "Text"); // add single value (will be selected)
* @example $("#myselect").addOption("Value 2", "Text 2", false); // add single value (won't be selected)
* @example $("#myselect").addOption({"foo":"bar","bar":"baz"}, false); // add multiple values, but don't select
*
*/
$.fn.addOption = function()
{
var add = function(el, v, t, sO)
{
var option = document.createElement("option");
option.value = v, option.text = t;
// get options
var o = el.options;
// get number of options
var oL = o.length;
if(!el.cache)
{
el.cache = {};
// loop through existing options, adding to cache
for(var i = 0; i < oL; i++)
{
el.cache[o[i].value] = i;
}
}
// add to cache if it isn't already
if(typeof el.cache[v] == "undefined") el.cache[v] = oL;
el.options[el.cache[v]] = option;
if(sO)
{
option.selected = true;
}
};
var a = arguments;
if(a.length == 0) return this;
// select option when added? default is true
var sO = true;
// multiple items
var m = false;
// other variables
var items, v, t;
if(typeof(a[0]) == "object")
{
m = true;
items = a[0];
}
if(a.length >= 2)
{
if(typeof(a[1]) == "boolean") sO = a[1];
else if(typeof(a[2]) == "boolean") sO = a[2];
if(!m)
{
v = a[0];
t = a[1];
}
}
this.each(
function()
{
if(this.nodeName.toLowerCase() != "select") return;
if(m)
{
for(var item in items)
{
add(this, item, items[item], sO);
}
}
else
{
add(this, v, t, sO);
}
}
);
return this;
};
/**
* Add options via ajax
*
* @name ajaxAddOption
* @author Sam Collett (http://www.texotela.co.uk)
* @type jQuery
* @param String url Page to get options from (must be valid JSON)
* @param Object params (optional) Any parameters to send with the request
* @param Boolean select (optional) Select the added options, default true
* @param Function fn (optional) Call this function with the select object as param after completion
* @param Array args (optional) Array with params to pass to the function afterwards
* @example $("#myselect").ajaxAddOption("myoptions.php");
* @example $("#myselect").ajaxAddOption("myoptions.php", {"code" : "007"});
* @example $("#myselect").ajaxAddOption("myoptions.php", {"code" : "007"}, false, sortoptions, {"dir": "desc"});
*
*/
$.fn.ajaxAddOption = function(url, params, select, fn, args)
{
if(typeof(url) != "string") return this;
if(typeof(params) != "object") params = {};
if(typeof(select) != "boolean") select = true;
this.each(
function()
{
var el = this;
$.getJSON(url,
params,
function(r)
{
$(el).addOption(r, select);
if(typeof fn == "function")
{
if(typeof args == "object")
{
fn.apply(el, args);
}
else
{
fn.call(el);
}
}
}
);
}
);
return this;
};
/**
* Removes an option (by value or index) from a select box (or series of select boxes)
*
* @name removeOption
* @author Sam Collett (http://www.texotela.co.uk)
* @type jQuery
* @param String|RegExp|Number what Option to remove
* @param Boolean selectedOnly (optional) Remove only if it has been selected (default false)
* @example $("#myselect").removeOption("Value"); // remove by value
* @example $("#myselect").removeOption(/^val/i); // remove options with a value starting with 'val'
* @example $("#myselect").removeOption(/./); // remove all options
* @example $("#myselect").removeOption(/./, true); // remove all options that have been selected
* @example $("#myselect").removeOption(0); // remove by index
*
*/
$.fn.removeOption = function()
{
var a = arguments;
if(a.length == 0) return this;
var ta = typeof(a[0]);
var v, index;
// has to be a string or regular expression (object in IE, function in Firefox)
if(ta == "string" || ta == "object" || ta == "function" ) v = a[0];
else if(ta == "number") index = a[0];
else return this;
this.each(
function()
{
if(this.nodeName.toLowerCase() != "select") return;
// clear cache
if(this.cache) this.cache = null;
// does the option need to be removed?
var remove = false;
// get options
var o = this.options;
if(!!v)
{
// get number of options
var oL = o.length;
for(var i=oL-1; i>=0; i--)
{
if(v.constructor == RegExp)
{
if(o[i].value.match(v))
{
remove = true;
}
}
else if(o[i].value == v)
{
remove = true;
}
// if the option is only to be removed if selected
if(remove && a[1] === true) remove = o[i].selected;
if(remove)
{
o[i] = null;
}
remove = false;
}
}
else
{
// only remove if selected?
if(a[1] === true)
{
remove = o[index].selected;
}
else
{
remove = true;
}
if(remove)
{
this.remove(index);
}
}
}
);
return this;
};
/**
* Sort options (ascending or descending) in a select box (or series of select boxes)
*
* @name sortOptions
* @author Sam Collett (http://www.texotela.co.uk)
* @type jQuery
* @param Boolean ascending (optional) Sort ascending (true/undefined), or descending (false)
* @example // ascending
* $("#myselect").sortOptions(); // or $("#myselect").sortOptions(true);
* @example // descending
* $("#myselect").sortOptions(false);
*
*/
$.fn.sortOptions = function(ascending)
{
var a = typeof(ascending) == "undefined" ? true : !!ascending;
this.each(
function()
{
if(this.nodeName.toLowerCase() != "select") return;
// get options
var o = this.options;
// get number of options
var oL = o.length;
// create an array for sorting
var sA = [];
// loop through options, adding to sort array
for(var i = 0; i<oL; i++)
{
sA[i] = {
v: o[i].value,
t: o[i].text
}
}
// sort items in array
sA.sort(
function(o1, o2)
{
// option text is made lowercase for case insensitive sorting
o1t = o1.t.toLowerCase(), o2t = o2.t.toLowerCase();
// if options are the same, no sorting is needed
if(o1t == o2t) return 0;
if(a)
{
return o1t < o2t ? -1 : 1;
}
else
{
return o1t > o2t ? -1 : 1;
}
}
);
// change the options to match the sort array
for(var i = 0; i<oL; i++)
{
o[i].text = sA[i].t;
o[i].value = sA[i].v;
}
}
);
return this;
};
/**
* Selects an option by value
*
* @name selectOptions
* @author Mathias Bank (http://www.mathias-bank.de), original function
* @author Sam Collett (http://www.texotela.co.uk), addition of regular expression matching
* @type jQuery
* @param String|RegExp value Which options should be selected
* can be a string or regular expression
* @param Boolean clear Clear existing selected options, default false
* @example $("#myselect").selectOptions("val1"); // with the value 'val1'
* @example $("#myselect").selectOptions(/^val/i); // with the value starting with 'val', case insensitive
*
*/
$.fn.selectOptions = function(value, clear)
{
var v = value;
var vT = typeof(value);
var c = clear || false;
// has to be a string or regular expression (object in IE, function in Firefox)
if(vT != "string" && vT != "function" && vT != "object") return this;
this.each(
function()
{
if(this.nodeName.toLowerCase() != "select") return this;
// get options
var o = this.options;
// get number of options
var oL = o.length;
for(var i = 0; i<oL; i++)
{
if(v.constructor == RegExp)
{
if(o[i].value.match(v))
{
o[i].selected = true;
}
else if(c)
{
o[i].selected = false;
}
}
else
{
if(o[i].value == v)
{
o[i].selected = true;
}
else if(c)
{
o[i].selected = false;
}
}
}
}
);
return this;
};
/**
* Copy options to another select
*
* @name copyOptions
* @author Sam Collett (http://www.texotela.co.uk)
* @type jQuery
* @param String to Element to copy to
* @param String which (optional) Specifies which options should be copied - 'all' or 'selected'. Default is 'selected'
* @example $("#myselect").copyOptions("#myselect2"); // copy selected options from 'myselect' to 'myselect2'
* @example $("#myselect").copyOptions("#myselect2","selected"); // same as above
* @example $("#myselect").copyOptions("#myselect2","all"); // copy all options from 'myselect' to 'myselect2'
*
*/
$.fn.copyOptions = function(to, which)
{
var w = which || "selected";
if($(to).size() == 0) return this;
this.each(
function()
{
if(this.nodeName.toLowerCase() != "select") return this;
// get options
var o = this.options;
// get number of options
var oL = o.length;
for(var i = 0; i<oL; i++)
{
if(w == "all" || (w == "selected" && o[i].selected))
{
$(to).addOption(o[i].value, o[i].text);
}
}
}
);
return this;
};
/**
* Checks if a select box has an option with the supplied value
*
* @name containsOption
* @author Sam Collett (http://www.texotela.co.uk)
* @type Boolean|jQuery
* @param String|RegExp value Which value to check for. Can be a string or regular expression
* @param Function fn (optional) Function to apply if an option with the given value is found.
* Use this if you don't want to break the chaining
* @example if($("#myselect").containsOption("val1")) alert("Has an option with the value 'val1'");
* @example if($("#myselect").containsOption(/^val/i)) alert("Has an option with the value starting with 'val'");
* @example $("#myselect").containsOption("val1", copyoption).doSomethingElseWithSelect(); // calls copyoption (user defined function) for any options found, chain is continued
*
*/
$.fn.containsOption = function(value, fn)
{
var found = false;
var v = value;
var vT = typeof(v);
var fT = typeof(fn);
// has to be a string or regular expression (object in IE, function in Firefox)
if(vT != "string" && vT != "function" && vT != "object") return fT == "function" ? this: found;
this.each(
function()
{
if(this.nodeName.toLowerCase() != "select") return this;
// option already found
if(found && fT != "function") return false;
// get options
var o = this.options;
// get number of options
var oL = o.length;
for(var i = 0; i<oL; i++)
{
if(v.constructor == RegExp)
{
if (o[i].value.match(v))
{
found = true;
if(fT == "function") fn.call(o[i]);
}
}
else
{
if (o[i].value == v)
{
found = true;
if(fT == "function") fn.call(o[i]);
}
}
}
}
);
return fT == "function" ? this : found;
};
/**
* Returns values which have been selected
*
* @name selectedValues
* @author Sam Collett (http://www.texotela.co.uk)
* @type Array
* @example $("#myselect").selectedValues();
*
*/
$.fn.selectedValues = function()
{
var v = [];
this.find("option:selected").each(
function()
{
v[v.length] = this.value;
}
);
return v;
};
})(jQuery);
\ No newline at end of file
/*
*
* TableSorter 2.0 - Client-side table sorting with ease!
* Version 2.0.1
* @requires jQuery v1.2.1
*
* Copyright (c) 2007 Christian Bach
* Examples and docs at: http://tablesorter.com
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
*/
/**
*
* @description Create a sortable table with multi-column sorting capabilitys
*
* @example $('table').tablesorter();
* @desc Create a simple tablesorter interface.
*
* @example $('table').tablesorter({ sortList:[[0,0],[1,0]] });
* @desc Create a tablesorter interface and sort on the first and secound column in ascending order.
*
* @example $('table').tablesorter({ headers: { 0: { sorter: false}, 1: {sorter: false} } });
* @desc Create a tablesorter interface and disableing the first and secound column headers.
*
* @example $('table').tablesorter({ 0: {sorter:"integer"}, 1: {sorter:"currency"} });
* @desc Create a tablesorter interface and set a column parser for the first and secound column.
*
*
* @param Object settings An object literal containing key/value pairs to provide optional settings.
*
* @option String cssHeader (optional) A string of the class name to be appended to sortable tr elements in the thead of the table.
* Default value: "header"
*
* @option String cssAsc (optional) A string of the class name to be appended to sortable tr elements in the thead on a ascending sort.
* Default value: "headerSortUp"
*
* @option String cssDesc (optional) A string of the class name to be appended to sortable tr elements in the thead on a descending sort.
* Default value: "headerSortDown"
*
* @option String sortInitialOrder (optional) A string of the inital sorting order can be asc or desc.
* Default value: "asc"
*
* @option String sortMultisortKey (optional) A string of the multi-column sort key.
* Default value: "shiftKey"
*
* @option String textExtraction (optional) A string of the text-extraction method to use.
* For complex html structures inside td cell set this option to "complex",
* on large tables the complex option can be slow.
* Default value: "simple"
*
* @option Object headers (optional) An array containing the forces sorting rules.
* This option let's you specify a default sorting rule.
* Default value: null
*
* @option Array sortList (optional) An array containing the forces sorting rules.
* This option let's you specify a default sorting rule.
* Default value: null
*
* @option Array sortForce (optional) An array containing the forces sorting rules.
* This option let's you specify a default sorting rule.
* Default value: null
*
*
* @option Boolean widthFixed (optional) Boolean flag indicating if tablesorter should apply fixed widths to the table columns.
* This is usefull when using the pager companion plugin.
* This options requires the dimension jquery plugin.
* Default value: false
*
* @option Boolean cancelSelection (optional) Boolean flag indicating if tablesorter should cancel selection of the table headers text.
* Default value: true
*
* @option Boolean debug (optional) Boolean flag indicating if tablesorter should display debuging information usefull for development.
*
* @type jQuery
*
* @name tablesorter
*
* @cat Plugins/Tablesorter
*
* @author Christian Bach/christian.bach@polyester.se
*/
(function($) {
$.extend({
tablesorter: new function() {
var parsers = [], widgets = [];
this.defaults = {
cssHeader: "header",
cssAsc: "headerSortUp",
cssDesc: "headerSortDown",
sortInitialOrder: "asc",
sortMultiSortKey: "shiftKey",
sortForce: null,
textExtraction: "simple",
parsers: {},
widgets: [],
widgetZebra: {css: ["even","odd"]},
headers: {},
widthFixed: false,
cancelSelection: true,
sortList: [],
headerList: [],
dateFormat: "us",
debug: false
};
/* debuging utils */
function benchmark(s,d) {
log(s + "," + (new Date().getTime() - d.getTime()) + "ms");
}
this.benchmark = benchmark;
function log(s) {
if (typeof console != "undefined" && typeof console.debug != "undefined") {
console.log(s);
} else {
alert(s);
}
}
/* parsers utils */
function buildParserCache(table,$headers) {
if(table.config.debug) { var parsersDebug = ""; }
var rows = table.tBodies[0].rows;
if(table.tBodies[0].rows[0]) {
var list = [], cells = rows[0].cells, l = cells.length;
for (var i=0;i < l; i++) {
var p = false;
if($.meta && ($($headers[i]).data() && $($headers[i]).data().sorter) ) {
p = getParserById($($headers[i]).data().sorter);
} else if((table.config.headers[i] && table.config.headers[i].sorter)) {
p = getParserById(table.config.headers[i].sorter);
}
if(!p) {
p = detectParserForColumn(table.config,cells[i]);
}
if(table.config.debug) { parsersDebug += "column:" + i + " parser:" +p.id + "\n"; }
list.push(p);
}
}
if(table.config.debug) { log(parsersDebug); }
return list;
};
function detectParserForColumn(config,node) {
var l = parsers.length;
for(var i=1; i < l; i++) {
if(parsers[i].is($.trim(getElementText(config,node)))) {
return parsers[i];
}
}
// 0 is always the generic parser (text)
return parsers[0];
}
function getParserById(name) {
var l = parsers.length;
for(var i=0; i < l; i++) {
if(parsers[i].id.toLowerCase() == name.toLowerCase()) {
return parsers[i];
}
}
return false;
}
/* utils */
function buildCache(table) {
if(table.config.debug) { var cacheTime = new Date(); }
var totalRows = (table.tBodies[0] && table.tBodies[0].rows.length) || 0,
totalCells = (table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length) || 0,
parsers = table.config.parsers,
cache = {row: [], normalized: []};
for (var i=0;i < totalRows; ++i) {
/** Add the table data to main data array */
var c = table.tBodies[0].rows[i], cols = [];
cache.row.push($(c));
for(var j=0; j < totalCells; ++j) {
cols.push(parsers[j].format(getElementText(table.config,c.cells[j]),table,c.cells[j]));
}
cols.push(i); // add position for rowCache
cache.normalized.push(cols);
cols = null;
};
if(table.config.debug) { benchmark("Building cache for " + totalRows + " rows:", cacheTime); }
return cache;
};
function getElementText(config,node) {
if(!node) return "";
var t = "";
if(config.textExtraction == "simple") {
if(node.childNodes[0] && node.childNodes[0].hasChildNodes()) {
t = node.childNodes[0].innerHTML;
} else {
t = node.innerHTML;
}
} else {
if(typeof(config.textExtraction) == "function") {
t = config.textExtraction(node);
} else {
t = $(node).text();
}
}
return t;
}
function appendToTable(table,cache) {
if(table.config.debug) {var appendTime = new Date()}
var c = cache,
r = c.row,
n= c.normalized,
totalRows = n.length,
checkCell = (n[0].length-1),
tableBody = $(table.tBodies[0]),
rows = [];
// clear the table body
//$.tablesorter.clearTableBody(table);
for (var i=0;i < totalRows; i++) {
rows.push(r[n[i][checkCell]]);
if(!table.config.appender) {
var o = r[n[i][checkCell]];
var l = o.length;
for(var j=0; j < l; j++) {
tableBody[0].appendChild(o[j]);
}
//tableBody.append(r[n[i][checkCell]]);
}
}
if(table.config.appender) {
table.config.appender(table,rows);
}
rows = null;
if(table.config.debug) { benchmark("Rebuilt table:", appendTime); }
//apply table widgets
applyWidget(table);
};
function buildHeaders(table) {
if(table.config.debug) { var time = new Date(); }
var meta = ($.meta) ? true : false, tableHeadersRows = [];
for(var i = 0; i < table.tHead.rows.length; i++) { tableHeadersRows[i]=0; };
$tableHeaders = $("thead th",table);
$tableHeaders.each(function(index) {
this.count = 0;
this.column = index;
this.order = formatSortingOrder(table.config.sortInitialOrder);
if(this.order)
this.count++;
if(checkHeaderMetadata(this) || checkHeaderOptions(table,index)) this.sortDisabled = true;
if(!this.sortDisabled) {
$(this).addClass(table.config.cssHeader);
}
// add cell to headerList
table.config.headerList[index]= this;
});
if(table.config.debug) { benchmark("Built headers:", time); log($tableHeaders); }
return $tableHeaders;
};
function checkCellColSpan(table, rows, row) {
var arr = [], r = table.tHead.rows, c = r[row].cells;
for(var i=0; i < c.length; i++) {
var cell = c[i];
if ( cell.colSpan > 1) {
arr = arr.concat(checkCellColSpan(table, headerArr,row++));
} else {
if(table.tHead.length == 1 || (cell.rowSpan > 1 || !r[row+1])) {
arr.push(cell);
}
//headerArr[row] = (i+row);
}
}
return arr;
};
function checkHeaderMetadata(cell) {
if(($.meta) && ($(cell).data().sorter === false)) { return true; };
return false;
}
function checkHeaderOptions(table,i) {
if((table.config.headers[i]) && (table.config.headers[i].sorter === false)) { return true; };
return false;
}
function applyWidget(table) {
var c = table.config.widgets;
var l = c.length;
for(var i=0; i < l; i++) {
getWidgetById(c[i]).format(table);
}
}
function getWidgetById(name) {
var l = widgets.length;
for(var i=0; i < l; i++) {
if(widgets[i].id.toLowerCase() == name.toLowerCase() ) {
return widgets[i];
}
}
};
function formatSortingOrder(v) {
if(typeof(v) != "Number") {
i = (v.toLowerCase() == "desc") ? 1 : 0;
} else {
i = (v == (0 || 1)) ? v : 0;
}
return i;
}
function isValueInArray(v, a) {
var l = a.length;
for(var i=0; i < l; i++) {
if(a[i][0] == v) {
return true;
}
}
return false;
}
function setHeadersCss(table,$headers, list, css) {
// remove all header information
$headers.removeClass(css[0]).removeClass(css[1]);
var h = [];
$headers.each(function(offset) {
if(!this.sortDisabled) {
h[this.column] = $(this);
}
});
var l = list.length;
for(var i=0; i < l; i++) {
h[list[i][0]].addClass(css[list[i][1]]);
}
}
function fixColumnWidth(table,$headers) {
var c = table.config;
if(c.widthFixed) {
var colgroup = $('<colgroup>');
$("tr:first td",table.tBodies[0]).each(function() {
colgroup.append($('<col>').css('width',$(this).width()));
});
$(table).prepend(colgroup);
};
}
function updateHeaderSortCount(table,sortList) {
var c = table.config, l = sortList.length;
for(var i=0; i < l; i++) {
var s = sortList[i], o = c.headerList[s[0]];
o.count = s[1];
o.count++;
}
}
/* sorting methods */
function multisort(table,sortList,cache) {
if(table.config.debug) { var sortTime = new Date(); }
var dynamicExp = "var sortWrapper = function(a,b) {", l = sortList.length;
for(var i=0; i < l; i++) {
var c = sortList[i][0];
var order = sortList[i][1];
var s = (getCachedSortType(table.config.parsers,c) == "text") ? ((order == 0) ? "sortText" : "sortTextDesc") : ((order == 0) ? "sortNumeric" : "sortNumericDesc");
var e = "e" + i;
dynamicExp += "var " + e + " = " + s + "(a[" + c + "],b[" + c + "]); ";
dynamicExp += "if(" + e + ") { return " + e + "; } ";
dynamicExp += "else { ";
}
// if value is the same keep orignal order
var orgOrderCol = cache.normalized[0].length - 1;
dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];";
for(var i=0; i < l; i++) {
dynamicExp += "}; ";
}
dynamicExp += "return 0; ";
dynamicExp += "}; ";
eval(dynamicExp);
cache.normalized.sort(sortWrapper);
if(table.config.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order+ " time:", sortTime); }
return cache;
};
function sortText(a,b) {
return ((a < b) ? -1 : ((a > b) ? 1 : 0));
};
function sortTextDesc(a,b) {
return ((b < a) ? -1 : ((b > a) ? 1 : 0));
};
function sortNumeric(a,b) {
return a-b;
};
function sortNumericDesc(a,b) {
return b-a;
};
function getCachedSortType(parsers,i) {
return parsers[i].type;
};
/* public methods */
this.construct = function(settings) {
return this.each(function() {
if(!this.tHead || !this.tBodies) return;
var $this, $document,$headers, cache, config, shiftDown = 0, sortOrder;
this.config = {};
config = $.extend(this.config, $.tablesorter.defaults, settings);
// store common expression for speed
$this = $(this);
// build headers
$headers = buildHeaders(this);
// try to auto detect column type, and store in tables config
this.config.parsers = buildParserCache(this,$headers);
// build the cache for the tbody cells
cache = buildCache(this);
// get the css class names, could be done else where.
var sortCSS = [config.cssDesc,config.cssAsc];
// fixate columns if the users supplies the fixedWidth option
fixColumnWidth(this);
// apply event handling to headers
// this is to big, perhaps break it out?
$headers.click(function(e) {
var totalRows = ($this[0].tBodies[0] && $this[0].tBodies[0].rows.length) || 0;
if(!this.sortDisabled && totalRows > 0) {
// store exp, for speed
var $cell = $(this);
// get current column index
var i = this.column;
this.order = (this.count++) % 2;
// user only whants to sort on one column
if(!e[config.sortMultiSortKey]) {
// flush the sort list
config.sortList = [];
if(config.sortForce != null) {
var a = config.sortForce;
for(var j=0; j < a.length; j++) {
config.sortList.push(a[j]);
}
}
// add column to sort list
config.sortList.push([i,this.order]);
// multi column sorting
} else {
// the user has clicked on an all ready sortet column.
if(isValueInArray(i,config.sortList)) {
// revers the sorting direction for all tables.
for(var j=0; j < config.sortList.length; j++) {
var s = config.sortList[j], o = config.headerList[s[0]];
if(s[0] == i) {
o.count = s[1];
o.count++;
s[1] = o.count % 2;
}
}
} else {
// add column to sort list array
config.sortList.push([i,this.order]);
}
};
// trigger sortstart
$this.trigger("sortStart");
//set css for headers
setHeadersCss($this[0],$headers,config.sortList,sortCSS);
// javascript threading..
setTimeout(function() {
// sort the table and append it to the dom
appendToTable($this[0],multisort($this[0],config.sortList,cache));
// trigger sortstart
$this.trigger("sortEnd");
}, 0);
// stop normal event by returning false
return false;
}
// cancel selection
}).mousedown(function() {
if(config.cancelSelection) {
this.onselectstart = function() {return false};
return false;
}
});
// apply easy methods that trigger binded events
$this.bind("update",function() {
// rebuild parsers.
this.config.parsers = buildParserCache(this,$headers);
// rebuild the cache map
cache = buildCache(this);
}).bind("sorton",function(e,list) {
config.sortList = list;
// update and store the sortlist
var sortList = config.sortList;
// update header count index
updateHeaderSortCount(this,sortList);
//set css for headers
setHeadersCss(this,$headers,sortList,sortCSS);
// sort the table and append it to the dom
appendToTable(this,multisort(this,sortList,cache));
}).bind("appendCache",function() {
appendToTable(this,cache);
}).bind("applyWidgetId",function(e,id) {
getWidgetById(id).format(this);
}).bind("applyWidgets",function() {
// apply widgets
applyWidget(this);
});
if($.meta && ($(this).data() && $(this).data().sortlist)) {
config.sortList = $(this).data().sortlist;
}
// if user has supplied a sort list to constructor.
if(config.sortList.length > 0) {
$this.trigger("sorton",[config.sortList]);
}
// apply widgets
applyWidget(this);
});
};
this.addParser = function(parser) {
var l = parsers.length, a = true;
for(var i=0; i < l; i++) {
if(parsers[i].id.toLowerCase() == parser.id.toLowerCase()) {
a = false;
}
}
if(a) { parsers.push(parser); };
};
this.addWidget = function(widget) {
widgets.push(widget);
};
this.formatFloat = function(s) {
var i = parseFloat(s);
return (isNaN(i)) ? 0 : i;
};
this.formatInt = function(s) {
var i = parseInt(s);
return (isNaN(i)) ? 0 : i;
};
this.clearTableBody = function(table) {
if($.browser.msie) {
function empty() {
while ( this.firstChild ) this.removeChild( this.firstChild );
}
empty.apply(table.tBodies[0]);
} else {
table.tBodies[0].innerHTML = "";
}
};
}
});
// extend plugin scope
$.fn.extend({
tablesorter: $.tablesorter.construct
});
var ts = $.tablesorter;
// add default parsers
ts.addParser({
id: "text",
is: function(s) {
return true;
},
format: function(s) {
return $.trim(s.toLowerCase());
},
type: "text"
});
ts.addParser({
id: "integer",
is: function(s) {
return /^\d+$/.test(s);
},
format: function(s) {
return $.tablesorter.formatFloat(s);
},
type: "numeric"
});
ts.addParser({
id: "currency",
is: function(s) {
return /^[£$€?.]/.test(s);
},
format: function(s) {
return $.tablesorter.formatFloat(s.replace(new RegExp(/[^0-9.]/g),""));
},
type: "numeric"
});
ts.addParser({
id: "floating",
is: function(s) {
return s.match(new RegExp(/^(\+|-)?[0-9]+\.[0-9]+((E|e)(\+|-)?[0-9]+)?$/));
},
format: function(s) {
return $.tablesorter.formatFloat(s.replace(new RegExp(/,/),""));
},
type: "numeric"
});
ts.addParser({
id: "ipAddress",
is: function(s) {
return /^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s);
},
format: function(s) {
var a = s.split("."), r = "", l = a.length;
for(var i = 0; i < l; i++) {
var item = a[i];
if(item.length == 2) {
r += "0" + item;
} else {
r += item;
}
}
return $.tablesorter.formatFloat(r);
},
type: "numeric"
});
ts.addParser({
id: "url",
is: function(s) {
return /^(https?|ftp|file):\/\/$/.test(s);
},
format: function(s) {
return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//),''));
},
type: "text"
});
ts.addParser({
id: "isoDate",
is: function(s) {
return /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s);
},
format: function(s) {
return $.tablesorter.formatFloat((s != "") ? new Date(s.replace(new RegExp(/-/g),"/")).getTime() : "0");
},
type: "numeric"
});
ts.addParser({
id: "percent",
is: function(s) {
return /^\d{1,3}%$/.test(s);
},
format: function(s) {
return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g),""));
},
type: "numeric"
});
ts.addParser({
id: "usLongDate",
is: function(s) {
return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/));
},
format: function(s) {
return $.tablesorter.formatFloat(new Date(s).getTime());
},
type: "numeric"
});
ts.addParser({
id: "shortDate",
is: function(s) {
return /\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s);
},
format: function(s,table) {
var c = table.config;
s = s.replace(/\-/g,"/");
if(c.dateFormat == "us") {
// reformat the string in ISO format
s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$1/$2");
} else if(c.dateFormat == "uk") {
//reformat the string in ISO format
s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$2/$1");
} else if(c.dateFormat == "dd/mm/yy" || c.dateFormat == "dd-mm-yy") {
s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/, "$1/$2/$3");
}
return $.tablesorter.formatFloat(new Date(s).getTime());
},
type: "numeric"
});
ts.addParser({
id: "time",
is: function(s) {
return /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s);
},
format: function(s) {
return $.tablesorter.formatFloat(new Date("2000/01/01 " + s).getTime());
},
type: "numeric"
});
ts.addParser({
id: "metadata",
is: function(s) {
return false;
},
format: function(s,table,cell) {
var c = table.config, p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName;
return $(cell).data()[p];
},
type: "numeric"
});
// add default widgets
ts.addWidget({
id: "zebra",
format: function(table) {
if(table.config.debug) { var time = new Date(); }
$("tr:visible",table.tBodies[0])
.filter(':even')
.removeClass(table.config.widgetZebra.css[1]).addClass(table.config.widgetZebra.css[0])
.end().filter(':odd')
.removeClass(table.config.widgetZebra.css[0]).addClass(table.config.widgetZebra.css[1]);
if(table.config.debug) { $.tablesorter.benchmark("Applying Zebra widget", time); }
}
});
})(jQuery);
\ No newline at end of file
/**
* sprintf() for JavaScript v.0.4
*
* Copyright (c) 2007 Alexandru Marasteanu <http://alexei.417.ro/>
* Thanks to David Baird (unit test and patch).
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 59 Temple
* Place, Suite 330, Boston, MA 02111-1307 USA
*/
function str_repeat(i, m) { for (var o = []; m > 0; o[--m] = i); return(o.join('')); }
function sprintf () {
var i = 0, a, f = arguments[i++], o = [], m, p, c, x;
while (f) {
if (m = /^[^\x25]+/.exec(f)) o.push(m[0]);
else if (m = /^\x25{2}/.exec(f)) o.push('%');
else if (m = /^\x25(?:(\d+)\$)?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(f)) {
if (((a = arguments[m[1] || i++]) == null) || (a == undefined)) throw("Too few arguments.");
if (/[^s]/.test(m[7]) && (typeof(a) != 'number'))
throw("Expecting number but found " + typeof(a));
switch (m[7]) {
case 'b': a = a.toString(2); break;
case 'c': a = String.fromCharCode(a); break;
case 'd': a = parseInt(a); break;
case 'e': a = m[6] ? a.toExponential(m[6]) : a.toExponential(); break;
case 'f': a = m[6] ? parseFloat(a).toFixed(m[6]) : parseFloat(a); break;
case 'o': a = a.toString(8); break;
case 's': a = ((a = String(a)) && m[6] ? a.substring(0, m[6]) : a); break;
case 'u': a = Math.abs(a); break;
case 'x': a = a.toString(16); break;
case 'X': a = a.toString(16).toUpperCase(); break;
}
a = (/[def]/.test(m[7]) && m[2] && a > 0 ? '+' + a : a);
c = m[3] ? m[3] == '0' ? '0' : m[3].charAt(1) : ' ';
x = m[5] - String(a).length;
p = m[5] ? str_repeat(c, x) : '';
o.push(m[4] ? a + p : p + a);
}
else throw ("Huh ?!");
f = f.substring(m[0].length);
}
return o.join('');
}
<?php
require 'Reader.php';
require 'Preprocessor.php';
/**
* Class handling access to data-files(original and preprocessed) for webgrind.
* @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
$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');
$invokeUrl = '';
while ((($line = fgets($fp)) !== FALSE) && !strlen($invokeUrl)){
if (preg_match('/^cmd: (.*)$/', $line, $parts)){
$invokeUrl = isset($parts[1]) ? $parts[1] : '';
}
}
fclose($fp);
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];
}
}
\ No newline at end of file
<?php
/**
* Class for preprocessing callgrind files.
*
* 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),
'invocationCount' => 0,
'nr' => $nextFuncNr++,
'count' => 0,
'summedSelfCost' => 0,
'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));
}
}
}
<?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;
/**
* 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
);
$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]
);
$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]
);
$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 {
$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);
}
}
#!/usr/bin/env python
#
# Copyright 2008-2009 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
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""Generate a dot graph from the output of several profilers."""
__author__ = "Jose Fonseca"
__version__ = "1.0"
import sys
import math
import os.path
import re
import textwrap
import optparse
import xml.parsers.expat
try:
# Debugging helper module
import debug
except ImportError:
pass
def times(x):
return u"%u\xd7" % (x,)
def percentage(p):
return "%.02f%%" % (p*100.0,)
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
tol = 2 ** -23
def ratio(numerator, denominator):
try:
ratio = float(numerator)/float(denominator)
except ZeroDivisionError:
# 0/0 is undefined, but 1.0 yields more useful results
return 1.0
if ratio < 0.0:
if ratio < -tol:
sys.stderr.write('warning: negative ratio (%s/%s)\n' % (numerator, denominator))
return 0.0
if ratio > 1.0:
if ratio > 1.0 + tol:
sys.stderr.write('warning: ratio greater than one (%s/%s)\n' % (numerator, denominator))
return 1.0
return ratio
class UndefinedEvent(Exception):
"""Raised when attempting to get an event which is undefined."""
def __init__(self, event):
Exception.__init__(self)
self.event = event
def __str__(self):
return 'unspecified event %s' % self.event.name
class Event(object):
"""Describe a kind of event, and its basic operations."""
def __init__(self, name, null, aggregator, formatter = str):
self.name = name
self._null = null
self._aggregator = aggregator
self._formatter = formatter
def __eq__(self, other):
return self is other
def __hash__(self):
return id(self)
def null(self):
return self._null
def aggregate(self, val1, val2):
"""Aggregate two event values."""
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
return self._formatter(val)
CALLS = Event("Calls", 0, add, times)
SAMPLES = Event("Samples", 0, add)
SAMPLES2 = Event("Samples", 0, add)
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)
class Object(object):
"""Base class for all objects in profile which can store events."""
def __init__(self, events=None):
if events is None:
self.events = {}
else:
self.events = events
def __hash__(self):
return id(self)
def __eq__(self, other):
return self is other
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:
del self.events[event]
else:
self.events[event] = value
class Call(Object):
"""A call between functions.
There should be at most one call object for every pair of functions.
"""
def __init__(self, callee_id):
Object.__init__(self)
self.callee_id = callee_id
self.ratio = None
self.weight = None
class Function(Object):
"""A function."""
def __init__(self, id, name):
Object.__init__(self)
self.id = id
self.name = name
self.module = None
self.process = None
self.calls = {}
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)))
self.calls[call.callee_id] = call
def get_call(self, callee_id):
if not callee_id in self.calls:
call = Call(callee_id)
call[SAMPLES] = 0
call[SAMPLES2] = 0
call[CALLS] = 0
self.calls[callee_id] = call
return self.calls[callee_id]
# TODO: write utility functions
def __repr__(self):
return self.name
class Cycle(Object):
"""A cycle made from recursive function calls."""
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:
self.add_function(other)
function.cycle = self
class Profile(Object):
"""The whole profile."""
def __init__(self):
Object.__init__(self)
self.functions = {}
self.cycles = []
def add_function(self, function):
if function.id in self.functions:
sys.stderr.write('warning: overwriting function %s (id %s)\n' % (function.name, str(function.id)))
self.functions[function.id] = function
def add_cycle(self, cycle):
self.cycles.append(cycle)
def validate(self):
"""Validate the edges."""
for function in self.functions.itervalues():
for callee_id in function.calls.keys():
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))
del function.calls[callee_id]
def find_cycles(self):
"""Find cycles using Tarjan's strongly connected components algorithm."""
# Apply the Tarjan's algorithm successively until all functions are visited
visited = set()
for function in self.functions.itervalues():
if function not in visited:
self._tarjan(function, 0, [], {}, {}, visited)
cycles = []
for function in self.functions.itervalues():
if function.cycle is not None and function.cycle not in cycles:
cycles.append(function.cycle)
self.cycles = cycles
if 0:
for cycle in cycles:
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.
See also:
- http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
"""
visited.add(function)
orders[function] = order
lowlinks[function] = order
order += 1
pos = len(stack)
stack.append(function)
for call in function.calls.itervalues():
callee = self.functions[call.callee_id]
# TODO: use a set to optimize lookup
if callee not in orders:
order = self._tarjan(callee, order, stack, orders, lowlinks, visited)
lowlinks[function] = min(lowlinks[function], lowlinks[callee])
elif callee in stack:
lowlinks[function] = min(lowlinks[function], orders[callee])
if lowlinks[function] == orders[function]:
# Strongly connected component found
members = stack[pos:]
del stack[pos:]
if len(members) > 1:
cycle = Cycle()
for member in members:
cycle.add_function(member)
return order
def call_ratios(self, event):
# Aggregate for incoming calls
cycle_totals = {}
for cycle in self.cycles:
cycle_totals[cycle] = 0.0
function_totals = {}
for function in self.functions.itervalues():
function_totals[function] = 0.0
for function in self.functions.itervalues():
for call in function.calls.itervalues():
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]
# Compute the ratios
for function in self.functions.itervalues():
for call in function.calls.itervalues():
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]
else:
total = function_totals[callee]
call.ratio = ratio(call[event], total)
def integrate(self, outevent, inevent):
"""Propagate function time ratio allong the function calls.
Must be called after finding the cycles.
See also:
- http://citeseer.ist.psu.edu/graham82gprof.html
"""
# Sanity checking
assert outevent not in self
for function in self.functions.itervalues():
assert outevent not in function
assert inevent in function
for call in function.calls.itervalues():
assert outevent not in call
if call.callee_id != function.id:
assert call.ratio is not None
# Aggregate the input for each cycle
for cycle in self.cycles:
total = inevent.null()
for function in self.functions.itervalues():
total = inevent.aggregate(total, function[inevent])
self[inevent] = total
# Integrate along the edges
total = inevent.null()
for function in self.functions.itervalues():
total = inevent.aggregate(total, function[inevent])
self._integrate_function(function, outevent, inevent)
self[outevent] = total
def _integrate_function(self, function, outevent, inevent):
if function.cycle is not None:
return self._integrate_cycle(function.cycle, outevent, inevent)
else:
if outevent not in function:
total = function[inevent]
for call in function.calls.itervalues():
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
callee = self.functions[call.callee_id]
subtotal = call.ratio *self._integrate_function(callee, outevent, inevent)
call[outevent] = subtotal
return subtotal
def _integrate_cycle(self, cycle, outevent, inevent):
if outevent not in cycle:
# Compute the outevent for the whole cycle
total = inevent.null()
for member in cycle.functions:
subtotal = member[inevent]
for call in member.calls.itervalues():
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():
if function.cycle is not cycle:
for call in function.calls.itervalues():
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():
ranks = {}
call_ratios = {}
partials = {}
self._rank_cycle_function(cycle, callee, 0, ranks)
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
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():
if call.callee_id != function.id:
callee = self.functions[call.callee_id]
if callee.cycle is cycle:
self._rank_cycle_function(cycle, callee, rank + 1, ranks)
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():
if call.callee_id != function.id:
callee = self.functions[call.callee_id]
if callee.cycle is cycle:
if ranks[callee] > ranks[function]:
call_ratios[callee] = call_ratios.get(callee, 0.0) + call.ratio
self._call_ratios_cycle(cycle, callee, ranks, call_ratios, visited)
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():
if call.callee_id != function.id:
callee = self.functions[call.callee_id]
if callee.cycle is not cycle:
assert outevent in call
partial += partial_ratio*call[outevent]
else:
if ranks[callee] > ranks[function]:
callee_partial = self._integrate_cycle_function(cycle, callee, partial_ratio, partials, ranks, call_ratios, outevent, inevent)
call_ratio = ratio(call.ratio, call_ratios[callee])
call_partial = call_ratio*callee_partial
try:
call[outevent] += call_partial
except UndefinedEvent:
call[outevent] = call_partial
partial += call_partial
partials[function] = partial
try:
function[outevent] += partial
except UndefinedEvent:
function[outevent] = partial
return partials[function]
def aggregate(self, event):
"""Aggregate an event for the whole profile."""
total = event.null()
for function in self.functions.itervalues():
try:
total = event.aggregate(total, function[event])
except UndefinedEvent:
return
self[event] = total
def ratio(self, outevent, inevent):
assert outevent not in self
assert inevent in self
for function in self.functions.itervalues():
assert outevent not in function
assert inevent in function
function[outevent] = ratio(function[inevent], self[inevent])
for call in function.calls.itervalues():
assert outevent not in call
if inevent in call:
call[outevent] = ratio(call[inevent], self[inevent])
self[outevent] = 1.0
def prune(self, node_thres, edge_thres):
"""Prune the profile"""
# compute the prune ratios
for function in self.functions.itervalues():
try:
function.weight = function[TOTAL_TIME_RATIO]
except UndefinedEvent:
pass
for call in function.calls.itervalues():
callee = self.functions[call.callee_id]
if TOTAL_TIME_RATIO in call:
# handle exact cases first
call.weight = call[TOTAL_TIME_RATIO]
else:
try:
# make a safe estimate
call.weight = min(function[TOTAL_TIME_RATIO], callee[TOTAL_TIME_RATIO])
except UndefinedEvent:
pass
# prune the nodes
for function_id in self.functions.keys():
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():
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,))
self._dump_events(function.events)
for call in function.calls.itervalues():
callee = self.functions[call.callee_id]
sys.stderr.write(' Call %s:\n' % (callee.name,))
self._dump_events(call.events)
for cycle in self.cycles:
sys.stderr.write('Cycle:\n')
self._dump_events(cycle.events)
for function in cycle.functions:
sys.stderr.write(' Function %s\n' % (function.name,))
def _dump_events(self, events):
for event, value in events.iteritems():
sys.stderr.write(' %s: %s\n' % (event.name, event.format(value)))
class Struct:
"""Masquerade a dictionary with a structure-like behavior."""
def __init__(self, attrs = None):
if attrs is None:
attrs = {}
self.__dict__['_attrs'] = attrs
def __getattr__(self, name):
try:
return self._attrs[name]
except KeyError:
raise AttributeError(name)
def __setattr__(self, name, value):
self._attrs[name] = value
def __str__(self):
return str(self._attrs)
def __repr__(self):
return repr(self._attrs)
class ParseError(Exception):
"""Raised when parsing to signal mismatches."""
def __init__(self, msg, line):
self.msg = msg
# TODO: store more source line information
self.line = line
def __str__(self):
return '%s: %r' % (self.msg, self.line)
class Parser:
"""Parser interface."""
def __init__(self):
pass
def parse(self):
raise NotImplementedError
class LineParser(Parser):
"""Base class for parsers that read line-based formats."""
def __init__(self, file):
Parser.__init__(self)
self._file = file
self.__line = None
self.__eof = False
self.line_no = 0
def readline(self):
line = self._file.readline()
if not line:
self.__line = ''
self.__eof = True
else:
self.line_no += 1
self.__line = line.rstrip('\r\n')
def lookahead(self):
assert self.__line is not None
return self.__line
def consume(self):
assert self.__line is not None
line = self.__line
self.readline()
return line
def eof(self):
assert self.__line is not None
return self.__eof
XML_ELEMENT_START, XML_ELEMENT_END, XML_CHARACTER_DATA, XML_EOF = range(4)
class XmlToken:
def __init__(self, type, name_or_data, attrs = None, line = None, column = None):
assert type in (XML_ELEMENT_START, XML_ELEMENT_END, XML_CHARACTER_DATA, XML_EOF)
self.type = type
self.name_or_data = name_or_data
self.attrs = attrs
self.line = line
self.column = column
def __str__(self):
if self.type == XML_ELEMENT_START:
return '<' + self.name_or_data + ' ...>'
if self.type == XML_ELEMENT_END:
return '</' + self.name_or_data + '>'
if self.type == XML_CHARACTER_DATA:
return self.name_or_data
if self.type == XML_EOF:
return 'end of file'
assert 0
class XmlTokenizer:
"""Expat based XML tokenizer."""
def __init__(self, fp, skip_ws = True):
self.fp = fp
self.tokens = []
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()
token = XmlToken(XML_ELEMENT_END, name, None, line, column)
self.tokens.append(token)
def handle_character_data(self, data):
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():
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:
self.tokens = []
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
if self.index >= len(self.tokens):
line, column = self.pos()
token = XmlToken(XML_EOF, None, None, line, column)
else:
token = self.tokens[self.index]
self.index += 1
return token
def pos(self):
return self.parser.CurrentLineNumber, self.parser.CurrentColumnNumber
class XmlTokenMismatch(Exception):
def __init__(self, expected, found):
self.expected = expected
self.found = found
def __str__(self):
return '%u:%u: %s expected, %s found' % (self.found.line, self.found.column, str(self.expected), str(self.found))
class XmlParser(Parser):
"""Base XML document parser."""
def __init__(self, fp):
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
def element_start(self, name):
while self.token.type == XML_CHARACTER_DATA:
self.consume()
if self.token.type != XML_ELEMENT_START:
raise XmlTokenMismatch(XmlToken(XML_ELEMENT_START, name), self.token)
if self.token.name_or_data != name:
raise XmlTokenMismatch(XmlToken(XML_ELEMENT_START, name), self.token)
attrs = self.token.attrs
self.consume()
return attrs
def element_end(self, name):
while self.token.type == XML_CHARACTER_DATA:
self.consume()
if self.token.type != XML_ELEMENT_END:
raise XmlTokenMismatch(XmlToken(XML_ELEMENT_END, name), self.token)
if self.token.name_or_data != name:
raise XmlTokenMismatch(XmlToken(XML_ELEMENT_END, name), self.token)
self.consume()
def character_data(self, strip = True):
data = ''
while self.token.type == XML_CHARACTER_DATA:
data += self.token.name_or_data
self.consume()
if strip:
data = data.strip()
return data
class GprofParser(Parser):
"""Parser for GNU gprof output.
See also:
- Chapter "Interpreting gprof's Output" from the GNU gprof manual
http://sourceware.org/binutils/docs-2.18/gprof/Call-Graph.html#Call-Graph
- File "cg_print.c" from the GNU gprof source code
http://sourceware.org/cgi-bin/cvsweb.cgi/~checkout~/src/gprof/cg_print.c?rev=1.12&cvsroot=src
"""
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 groupdict.iteritems():
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(
# original gprof header
r'^\s+called/total\s+parents\s*$|' +
r'^index\s+%time\s+self\s+descendents\s+called\+self\s+name\s+index\s*$|' +
r'^\s+called/total\s+children\s*$|' +
# GNU gprof header
r'^index\s+%\s+time\s+self\s+children\s+called\s+name\s*$'
)
_cg_ignore_re = re.compile(
# spontaneous
r'^\s+<spontaneous>\s*$|'
# internal calls (such as "mcount")
r'^.*\((\d+)\)$'
)
_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'\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<name>\S.*?)' +
r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
r'\s\[(?P<index>\d+)\]$'
)
_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'\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<name>\S.*?)' +
r'(?:\s+<cycle\s(?P<cycle>\d+)>)?' +
r'\s\[(?P<index>\d+)\]$'
)
_cg_sep_re = re.compile(r'^--+$')
def parse_function_entry(self, lines):
parents = []
children = []
while True:
if not lines:
sys.stderr.write('warning: unexpected end of entry\n')
line = lines.pop(0)
if line.startswith('['):
break
# read function parent line
mo = self._cg_parent_re.match(line)
if not mo:
if self._cg_ignore_re.match(line):
continue
sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
else:
parent = self.translate(mo)
parents.append(parent)
# read primary line
mo = self._cg_primary_re.match(line)
if not mo:
sys.stderr.write('warning: unrecognized call graph entry: %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:
if self._cg_ignore_re.match(line):
continue
sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
else:
child = self.translate(mo)
children.append(child)
function.parents = parents
function.children = children
self.functions[function.index] = function
def parse_cycle_entry(self, lines):
# read cycle header line
line = lines[0]
mo = self._cg_cycle_header_re.match(line)
if not mo:
sys.stderr.write('warning: unrecognized call graph entry: %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: %r\n' % line)
continue
call = self.translate(mo)
cycle.functions.append(call)
self.cycles[cycle.cycle] = cycle
def parse_cg_entry(self, lines):
if lines[0].startswith("["):
self.parse_cycle_entry(lines)
else:
self.parse_function_entry(lines)
def parse_cg(self):
"""Parse the call graph."""
# skip call graph header
while not self._cg_header_re.match(self.readline()):
pass
line = self.readline()
while self._cg_header_re.match(line):
line = self.readline()
# process call graph entries
entry_lines = []
while line != '\014': # form feed
if line and not line.isspace():
if self._cg_sep_re.match(line):
self.parse_cg_entry(entry_lines)
entry_lines = []
else:
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()
for entry in self.functions.itervalues():
# populate the function
function = Function(entry.index, entry.name)
function[TIME] = entry.self
if entry.called is not None:
function.called = entry.called
if entry.called_self is not None:
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
# static call graph analysis dont have a call graph entry so we need
# to add them here
missing = Function(child.index, child.name)
function[TIME] = 0.0
function.called = 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 cycles.itervalues():
profile.add_cycle(cycle)
# Compute derived events
profile.validate()
profile.ratio(TIME_RATIO, TIME)
profile.call_ratios(CALLS)
profile.integrate(TOTAL_TIME, TIME)
profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME)
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+)+$')
def __init__(self, infile):
LineParser.__init__(self, infile)
# Textual positions
self.position_ids = {}
self.positions = {}
# Numeric positions
self.num_positions = 1
self.cost_positions = ['line']
self.last_positions = [0]
# Events
self.num_events = 0
self.cost_events = []
self.profile = Profile()
self.profile[SAMPLES] = 0
def parse(self):
# read lookahead
self.readline()
self.parse_key('version')
self.parse_key('creator')
self.parse_part()
# compute derived data
self.profile.validate()
self.profile.find_cycles()
self.profile.ratio(TIME_RATIO, SAMPLES)
self.profile.call_ratios(CALLS)
self.profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
return self.profile
def parse_part(self):
while self.parse_header_line():
pass
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):
return \
self.parse_empty() or \
self.parse_comment() or \
self.parse_part_detail() or \
self.parse_description() or \
self.parse_event_specification() or \
self.parse_cost_line_def() or \
self.parse_cost_summary()
_detail_keys = set(('cmd', 'pid', 'thread', 'part'))
def parse_part_detail(self):
return self.parse_keys(self._detail_keys)
def parse_description(self):
return self.parse_key('desc') is not None
def parse_event_specification(self):
event = self.parse_key('event')
if event is None:
return False
return True
def parse_cost_line_def(self):
pair = self.parse_keys(('events', 'positions'))
if pair is None:
return False
key, value = pair
items = value.split()
if key == 'events':
self.num_events = len(items)
self.cost_events = items
if key == 'positions':
self.num_positions = len(items)
self.cost_positions = items
self.last_positions = [0]*self.num_positions
return True
def parse_cost_summary(self):
pair = self.parse_keys(('summary', 'totals'))
if pair is None:
return False
return True
def parse_body_line(self):
return \
self.parse_empty() or \
self.parse_comment() or \
self.parse_cost_line() or \
self.parse_position_spec() or \
self.parse_association_spec()
__subpos_re = r'(0x[0-9a-fA-F]+|\d+|\+\d+|-\d+|\*)'
_cost_re = re.compile(r'^' +
__subpos_re + r'( +' + __subpos_re + r')*' +
r'( +\d+)*' +
'$')
def parse_cost_line(self, calls=None):
line = self.lookahead().rstrip()
mo = self._cost_re.match(line)
if not mo:
return False
function = self.get_function()
values = line.split(' ')
assert len(values) <= self.num_positions + self.num_events
positions = values[0 : self.num_positions]
events = values[self.num_positions : ]
events += ['0']*(self.num_events - len(events))
for i in range(self.num_positions):
position = positions[i]
if position == '*':
position = self.last_positions[i]
elif position[0] in '-+':
position = self.last_positions[i] + int(position)
elif position.startswith('0x'):
position = int(position, 16)
else:
position = int(position)
self.last_positions[i] = position
events = map(float, events)
if calls is None:
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]
function.add_call(call)
else:
call[CALLS] += calls
call[SAMPLES] += events[0]
self.consume()
return True
def parse_association_spec(self):
line = self.lookahead()
if not line.startswith('calls='):
return False
_, values = line.split('=', 1)
values = values.strip().split()
calls = int(values[0])
call_position = values[1:]
self.consume()
self.parse_cost_line(calls)
return True
_position_re = re.compile('^(?P<position>[cj]?(?:ob|fl|fi|fe|fn))=\s*(?:\((?P<id>\d+)\))?(?:\s*(?P<name>.+))?')
_position_table_map = {
'ob': 'ob',
'fl': 'fl',
'fi': 'fl',
'fe': 'fl',
'fn': 'fn',
'cob': 'ob',
'cfl': 'fl',
'cfi': 'fl',
'cfe': 'fl',
'cfn': 'fn',
'jfi': 'fl',
}
_position_map = {
'ob': 'ob',
'fl': 'fl',
'fi': 'fl',
'fe': 'fl',
'fn': 'fn',
'cob': 'cob',
'cfl': 'cfl',
'cfi': 'cfl',
'cfe': 'cfl',
'cfn': 'cfn',
'jfi': 'jfi',
}
def parse_position_spec(self):
line = self.lookahead()
if line.startswith('jump=') or line.startswith('jcnd='):
self.consume()
return True
mo = self._position_re.match(line)
if not mo:
return False
position, id, name = mo.groups()
if id:
table = self._position_table_map[position]
if name:
self.position_ids[(table, id)] = name
else:
name = self.position_ids.get((table, id), '')
self.positions[self._position_map[position]] = name
self.consume()
return True
def parse_empty(self):
if self.eof():
return False
line = self.lookahead()
if line.strip():
return False
self.consume()
return True
def parse_comment(self):
line = self.lookahead()
if not line.startswith('#'):
return False
self.consume()
return True
_key_re = re.compile(r'^(\w+):')
def parse_key(self, key):
pair = self.parse_keys((key,))
if not pair:
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()
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 make_function(self, module, filename, name):
# FIXME: module and filename are not being tracked reliably
#id = '|'.join((module, filename, name))
id = name
try:
function = self.profile.functions[id]
except KeyError:
function = Function(id, name)
function[SAMPLES] = 0
function.called = 0
self.profile.add_function(function)
return function
def get_function(self):
module = self.positions.get('ob', '')
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', '')
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
"""
_fields_re = {
'samples': r'(\d+)',
'%': r'(\S+)',
'linenr info': r'(?P<source>\(no location information\)|\S+:\d+)',
'image name': r'(?P<image>\S+(?:\s\(tgid:[^)]*\))?)',
'app name': r'(?P<application>\S+)',
'symbol name': r'(?P<symbol>\(no symbols\)|.+?)',
}
def __init__(self, infile):
LineParser.__init__(self, infile)
self.entries = {}
self.entry_re = None
def add_entry(self, callers, function, callees):
try:
entry = self.entries[function.id]
except KeyError:
self.entries[function.id] = (callers, function, callees)
else:
callers_total, function_total, callees_total = entry
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:
total = totals[partial.id]
except KeyError:
totals[partial.id] = partial
else:
total.samples += partial.samples
def parse(self):
# read lookahead
self.readline()
self.parse_header()
while self.lookahead():
self.parse_entry()
profile = Profile()
reverse_call_samples = {}
# populate the profile
profile[SAMPLES] = 0
for _callers, _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.application:
function.process = os.path.basename(_function.application)
if _function.image:
function.module = os.path.basename(_function.image)
total_callee_samples = 0
for _callee in _callees.itervalues():
total_callee_samples += _callee.samples
for _callee in _callees.itervalues():
if not _callee.self:
call = Call(_callee.id)
call[SAMPLES2] = _callee.samples
function.add_call(call)
# 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
def parse_header(self):
while not self.match_header():
self.consume()
line = self.lookahead()
fields = re.split(r'\s\s+', line)
entry_re = r'^\s*' + r'\s+'.join([self._fields_re[field] for field in fields]) + r'(?P<self>\s+\[self\])?$'
self.entry_re = re.compile(entry_re)
self.skip_separator()
def parse_entry(self):
callers = self.parse_subentries()
if self.match_primary():
function = self.parse_subentry()
if function is not None:
callees = self.parse_subentries()
self.add_entry(callers, function, callees)
self.skip_separator()
def parse_subentries(self):
subentries = {}
while self.match_secondary():
subentry = self.parse_subentry()
subentries[subentry.id] = subentry
return subentries
def parse_subentry(self):
entry = Struct()
line = self.consume()
mo = self.entry_re.match(line)
if not mo:
raise ParseError('failed to parse', line)
fields = mo.groupdict()
entry.samples = int(mo.group(1))
if 'source' in fields and fields['source'] != '(no location information)':
source = fields['source']
filename, lineno = source.split(':')
entry.filename = filename
entry.lineno = int(lineno)
else:
source = ''
entry.filename = None
entry.lineno = None
entry.image = fields.get('image', '')
entry.application = fields.get('application', '')
if 'symbol' in fields and fields['symbol'] != '(no symbols)':
entry.symbol = fields['symbol']
else:
entry.symbol = ''
if entry.symbol.startswith('"') and entry.symbol.endswith('"'):
entry.symbol = entry.symbol[1:-1]
entry.id = ':'.join((entry.application, entry.image, source, entry.symbol))
entry.self = fields.get('self', None) != None
if entry.self:
entry.id += ':self'
if entry.symbol:
entry.name = entry.symbol
else:
entry.name = entry.image
return entry
def skip_separator(self):
while not self.match_separator():
self.consume()
self.consume()
def match_header(self):
line = self.lookahead()
return line.startswith('samples')
def match_separator(self):
line = self.lookahead()
return line == '-'*len(line)
def match_primary(self):
line = self.lookahead()
return not line[:1].isspace()
def match_secondary(self):
line = self.lookahead()
return line[:1].isspace()
class HProfParser(LineParser):
"""Parser for java hprof output
See also:
- http://java.sun.com/developer/technicalArticles/Programming/HPROF.html
"""
trace_re = re.compile(r'\t(.*)\((.*):(.*)\)')
trace_id_re = re.compile(r'^TRACE (\d+):$')
def __init__(self, infile):
LineParser.__init__(self, infile)
self.traces = {}
self.samples = {}
def parse(self):
# read lookahead
self.readline()
while not self.lookahead().startswith('------'): self.consume()
while not self.lookahead().startswith('TRACE '): self.consume()
self.parse_traces()
while not self.lookahead().startswith('CPU'):
self.consume()
self.parse_samples()
# populate the profile
profile = Profile()
profile[SAMPLES] = 0
functions = {}
# build up callgraph
for id, trace in self.traces.iteritems():
if not id in self.samples: continue
mtime = self.samples[id][0]
last = None
for func, file, line in trace:
if not func in functions:
function = Function(func, func)
function[SAMPLES] = 0
profile.add_function(function)
functions[func] = function
function = functions[func]
# allocate time to the deepest method in the trace
if not last:
function[SAMPLES] += mtime
profile[SAMPLES] += mtime
else:
c = function.get_call(last)
c[SAMPLES2] += mtime
last = func
# 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
def parse_traces(self):
while self.lookahead().startswith('TRACE '):
self.parse_trace()
def parse_trace(self):
l = self.consume()
mo = self.trace_id_re.match(l)
tid = mo.group(1)
last = None
trace = []
while self.lookahead().startswith('\t'):
l = self.consume()
match = self.trace_re.search(l)
if not match:
#sys.stderr.write('Invalid line: %s\n' % l)
break
else:
function_name, file, line = match.groups()
trace += [(function_name, file, line)]
self.traces[int(tid)] = trace
def parse_samples(self):
self.consume()
self.consume()
while not self.lookahead().startswith('CPU'):
rank, percent_self, percent_accum, count, traceid, method = self.lookahead().split()
self.samples[int(traceid)] = (int(count), method)
self.consume()
class SysprofParser(XmlParser):
def __init__(self, stream):
XmlParser.__init__(self, stream)
def parse(self):
objects = {}
nodes = {}
self.element_start('profile')
while self.token.type == XML_ELEMENT_START:
if self.token.name_or_data == 'objects':
assert not objects
objects = self.parse_items('objects')
elif self.token.name_or_data == 'nodes':
assert not nodes
nodes = self.parse_items('nodes')
else:
self.parse_value(self.token.name_or_data)
self.element_end('profile')
return self.build_profile(objects, nodes)
def parse_items(self, name):
assert name[-1] == 's'
items = {}
self.element_start(name)
while self.token.type == XML_ELEMENT_START:
id, values = self.parse_item(name[:-1])
assert id not in items
items[id] = values
self.element_end(name)
return items
def parse_item(self, name):
attrs = self.element_start(name)
id = int(attrs['id'])
values = self.parse_values()
self.element_end(name)
return id, values
def parse_values(self):
values = {}
while self.token.type == XML_ELEMENT_START:
name = self.token.name_or_data
value = self.parse_value(name)
assert name not in values
values[name] = value
return values
def parse_value(self, tag):
self.element_start(tag)
value = self.character_data()
self.element_end(tag)
if value.isdigit():
return int(value)
if value.startswith('"') and value.endswith('"'):
return value[1:-1]
return value
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.)
if object['self'] == 0:
continue
function = Function(id, object['name'])
function[SAMPLES] = object['self']
profile.add_function(function)
profile[SAMPLES] += function[SAMPLES]
for id, node in nodes.iteritems():
# Ignore fake calls
if node['self'] == 0:
continue
# Find a non-ignored parent
parent_id = node['parent']
while parent_id != 0:
parent = nodes[parent_id]
caller_id = parent['object']
if objects[caller_id]['self'] != 0:
break
parent_id = parent['parent']
if parent_id == 0:
continue
callee_id = node['object']
assert objects[caller_id]['self']
assert objects[callee_id]['self']
function = profile.functions[caller_id]
samples = node['self']
try:
call = function.calls[callee_id]
except KeyError:
call = Call(callee_id)
call[SAMPLES2] = samples
function.add_call(call)
else:
call[SAMPLES2] += samples
# Compute derived events
profile.validate()
profile.find_cycles()
profile.ratio(TIME_RATIO, SAMPLES)
profile.call_ratios(SAMPLES2)
profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
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.
"""
def __init__(self, stream):
Parser.__init__(self)
self.stream = stream
self.profile = Profile()
self.profile[SAMPLES] = 0
self.column = {}
def parse(self):
import csv
reader = csv.reader(
self.stream,
delimiter = ',',
quotechar = None,
escapechar = None,
doublequote = False,
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)
# compute derived data
self.profile.validate()
self.profile.find_cycles()
self.profile.ratio(TIME_RATIO, SAMPLES)
self.profile.call_ratios(SAMPLES2)
self.profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
return self.profile
def parse_header(self, row):
for column in range(len(row)):
name = row[column]
assert name not in self.column
self.column[name] = column
def parse_row(self, row):
fields = {}
for name, column in self.column.iteritems():
value = row[column]
for factory in int, float:
try:
value = factory(value)
except ValueError:
pass
else:
break
fields[name] = value
process = fields['Process Name']
symbol = fields['Module'] + '!' + fields['Function']
weight = fields['Weight']
count = fields['Count']
function = self.get_function(process, symbol)
function[SAMPLES] += weight * count
self.profile[SAMPLES] += weight * count
stack = fields['Stack']
if stack != '?':
stack = stack.split('/')
assert stack[0] == '[Root]'
if stack[-1] != symbol:
# XXX: some cases the sampled function does not appear in the stack
stack.append(symbol)
caller = None
for symbol in stack[1:]:
callee = self.get_function(process, symbol)
if caller is not None:
try:
call = caller.calls[callee.id]
except KeyError:
call = Call(callee.id)
call[SAMPLES2] = count
caller.add_call(call)
else:
call[SAMPLES2] += count
caller = callee
def get_function(self, process, symbol):
function_id = process + '!' + symbol
try:
function = self.profile.functions[function_id]
except KeyError:
module, name = symbol.split('!', 1)
function = Function(function_id, name)
function.process = process
function.module = module
function[SAMPLES] = 0
self.profile.add_function(function)
return function
class SleepyParser(Parser):
"""Parser for GNU gprof output.
See also:
- http://www.codersnotes.com/sleepy/
- http://sleepygraph.sourceforge.net/
"""
def __init__(self, filename):
Parser.__init__(self)
from zipfile import ZipFile
self.database = ZipFile(filename)
self.symbols = {}
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'\s+(?P<sourceline>\d+)$'
)
def parse_symbols(self):
lines = self.database.read('symbols.txt').splitlines()
for line in lines:
mo = self._symbol_re.match(line)
if mo:
symbol_id, module, procname, sourcefile, sourceline = mo.groups()
function_id = ':'.join([module, procname])
try:
function = self.profile.functions[function_id]
except KeyError:
function = Function(function_id, procname)
function.module = module
function[SAMPLES] = 0
self.profile.add_function(function)
self.symbols[symbol_id] = function
def parse_callstacks(self):
lines = self.database.read("callstacks.txt").splitlines()
for line in lines:
fields = line.split()
samples = int(fields[0])
callstack = fields[1:]
callstack = [self.symbols[symbol_id] for symbol_id in callstack]
callee = callstack[0]
callee[SAMPLES] += samples
self.profile[SAMPLES] += samples
for caller in callstack[1:]:
try:
call = caller.calls[callee.id]
except KeyError:
call = Call(callee.id)
call[SAMPLES2] = samples
caller.add_call(call)
else:
call[SAMPLES2] += samples
callee = caller
def parse(self):
profile = self.profile
profile[SAMPLES] = 0
self.parse_symbols()
self.parse_callstacks()
# Compute derived events
profile.validate()
profile.find_cycles()
profile.ratio(TIME_RATIO, SAMPLES)
profile.call_ratios(SAMPLES2)
profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
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."""
def __init__(self, *filename):
import pstats
try:
self.stats = pstats.Stats(*filename)
except ValueError:
import hotshot.stats
self.stats = hotshot.stats.load(filename[0])
self.profile = Profile()
self.function_ids = {}
def get_function_name(self, (filename, line, name)):
module = os.path.splitext(filename)[0]
module = os.path.basename(module)
return "%s:%d:%s" % (module, line, name)
def get_function(self, key):
try:
id = self.function_ids[key]
except KeyError:
id = len(self.function_ids)
name = self.get_function_name(key)
function = Function(id, name)
self.profile.functions[id] = function
self.function_ids[key] = id
else:
function = self.profile.functions[id]
return function
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():
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():
caller = self.get_function(fn)
call = Call(callee.id)
if isinstance(value, tuple):
for i in xrange(0, len(value), 4):
nc, cc, tt, ct = value[i:i+4]
if CALLS in call:
call[CALLS] += cc
else:
call[CALLS] = cc
if TOTAL_TIME in call:
call[TOTAL_TIME] += ct
else:
call[TOTAL_TIME] = ct
else:
call[CALLS] = value
call[TOTAL_TIME] = ratio(value, nc)*ct
caller.add_call(call)
#self.stats.print_stats()
#self.stats.print_callees()
# Compute derived events
self.profile.validate()
self.profile.ratio(TIME_RATIO, TIME)
self.profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME)
return self.profile
class Theme:
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",
minfontsize = 10.0,
maxfontsize = 10.0,
minpenwidth = 0.5,
maxpenwidth = 4.0,
gamma = 2.2,
skew = 1.0):
self.bgcolor = bgcolor
self.mincolor = mincolor
self.maxcolor = maxcolor
self.fontname = fontname
self.minfontsize = minfontsize
self.maxfontsize = maxfontsize
self.minpenwidth = minpenwidth
self.maxpenwidth = maxpenwidth
self.gamma = gamma
self.skew = skew
def graph_bgcolor(self):
return self.hsl_to_rgb(*self.bgcolor)
def graph_fontname(self):
return self.fontname
def graph_fontsize(self):
return self.minfontsize
def node_bgcolor(self, weight):
return self.color(weight)
def node_fgcolor(self, weight):
return self.graph_bgcolor()
def node_fontsize(self, weight):
return self.fontsize(weight)
def edge_color(self, weight):
return self.color(weight)
def edge_fontsize(self, weight):
return self.fontsize(weight)
def edge_penwidth(self, weight):
return max(weight*self.maxpenwidth, self.minpenwidth)
def edge_arrowsize(self, weight):
return 0.5 * math.sqrt(self.edge_penwidth(weight))
def fontsize(self, weight):
return max(weight**2 * self.maxfontsize, self.minfontsize)
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:
h = hmin + weight*(hmax - hmin)
s = smin + weight*(smax - smin)
l = lmin + weight*(lmax - lmin)
else:
base = self.skew
h = hmin + ((hmax-hmin)*(-1.0 + (base ** weight)) / (base - 1.0))
s = smin + ((smax-smin)*(-1.0 + (base ** weight)) / (base - 1.0))
l = lmin + ((lmax-lmin)*(-1.0 + (base ** weight)) / (base - 1.0))
return self.hsl_to_rgb(h, s, l)
def hsl_to_rgb(self, h, s, l):
"""Convert a color from HSL color-model to RGB.
See also:
- http://www.w3.org/TR/css3-color/#hsl-color
"""
h = h % 1.0
s = min(max(s, 0.0), 1.0)
l = min(max(l, 0.0), 1.0)
if l <= 0.5:
m2 = l*(s + 1.0)
else:
m2 = l + s - l*s
m1 = l*2.0 - m2
r = self._hue_to_rgb(m1, m2, h + 1.0/3.0)
g = self._hue_to_rgb(m1, m2, h)
b = self._hue_to_rgb(m1, m2, h - 1.0/3.0)
# Apply gamma correction
r **= self.gamma
g **= self.gamma
b **= self.gamma
return (r, g, b)
def _hue_to_rgb(self, m1, m2, h):
if h < 0.0:
h += 1.0
elif h > 1.0:
h -= 1.0
if h*6 < 1.0:
return m1 + (m2 - m1)*h*6.0
elif h*2 < 1.0:
return m2
elif h*3 < 2.0:
return m1 + (m2 - m1)*(2.0/3.0 - h)*6.0
else:
return m1
TEMPERATURE_COLORMAP = Theme(
mincolor = (2.0/3.0, 0.80, 0.25), # dark blue
maxcolor = (0.0, 1.0, 0.5), # satured red
gamma = 1.0
)
PINK_COLORMAP = Theme(
mincolor = (0.0, 1.0, 0.90), # pink
maxcolor = (0.0, 1.0, 0.5), # satured red
)
GRAY_COLORMAP = Theme(
mincolor = (0.0, 0.0, 0.85), # light gray
maxcolor = (0.0, 0.0, 0.0), # black
)
BW_COLORMAP = Theme(
minfontsize = 8.0,
maxfontsize = 24.0,
mincolor = (0.0, 0.0, 0.0), # black
maxcolor = (0.0, 0.0, 0.0), # black
minpenwidth = 0.1,
maxpenwidth = 8.0,
)
class DotWriter:
"""Writer for the DOT language.
See also:
- "The DOT Language" specification
http://www.graphviz.org/doc/info/lang.html
"""
def __init__(self, fp):
self.fp = fp
def graph(self, profile, theme):
self.begin_graph()
fontname = theme.graph_fontname()
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('edge', fontname=fontname)
for function in profile.functions.itervalues():
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 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,))
if function.weight is not None:
weight = function.weight
else:
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)),
fontsize = "%.2f" % theme.node_fontsize(weight),
)
for call in function.calls.itervalues():
callee = profile.functions[call.callee_id]
labels = []
for event in TOTAL_TIME_RATIO, CALLS:
if event in call.events:
label = event.format(call[event])
labels.append(label)
if call.weight is not None:
weight = call.weight
elif callee.weight is not None:
weight = callee.weight
else:
weight = 0.0
label = '\n'.join(labels)
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),
arrowsize = "%.2f" % theme.edge_arrowsize(weight),
)
self.end_graph()
def begin_graph(self):
self.write('digraph {\n')
def end_graph(self):
self.write('}\n')
def attr(self, what, **attrs):
self.write("\t")
self.write(what)
self.attr_list(attrs)
self.write(";\n")
def node(self, node, **attrs):
self.write("\t")
self.id(node)
self.attr_list(attrs)
self.write(";\n")
def edge(self, src, dst, **attrs):
self.write("\t")
self.id(src)
self.write(" -> ")
self.id(dst)
self.attr_list(attrs)
self.write(";\n")
def attr_list(self, attrs):
if not attrs:
return
self.write(' [')
first = True
for name, value in attrs.iteritems():
if first:
first = False
else:
self.write(", ")
self.id(name)
self.write('=')
self.id(value)
self.write(']')
def id(self, id):
if isinstance(id, (int, float)):
s = str(id)
elif isinstance(id, basestring):
if id.isalnum() and not id.startswith('0x'):
s = id
else:
s = self.escape(id)
else:
raise TypeError
self.write(s)
def color(self, (r, g, b)):
def float2int(f):
if f <= 0.0:
return 0
if f >= 1.0:
return 255
return int(255.0*f + 0.5)
return "#" + "".join(["%02x" % float2int(c) for c in (r, g, b)])
def escape(self, s):
s = s.encode('utf-8')
s = s.replace('\\', r'\\')
s = s.replace('\n', r'\n')
s = s.replace('\t', r'\t')
s = s.replace('"', r'\"')
return '"' + s + '"'
def write(self, s):
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."""
# 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
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
def compress_function_name(self, name):
"""Compress function name according to the user preferences."""
if self.options.strip:
name = self.strip_function_name(name)
if self.options.wrap:
name = self.wrap_function_name(name)
# TODO: merge functions with same resulting name
return name
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)
for function in profile.functions.itervalues():
function.name = self.compress_function_name(function.name)
dot.graph(profile, self.theme)
if __name__ == '__main__':
Main().main()
\ No newline at end of file
Software License Agreement (BSD License)
Copyright (c) 2008-2011, Jacob Oettinger & Joakim Nygård
All rights reserved.
Redistribution and use of this software in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
* Redistributions of source code must retain the above
copyright notice, this list of conditions and the
following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the
following disclaimer in the documentation and/or other
materials provided with the distribution.
* Neither the name of Jacob Oettinger and Joakim Nygård nor the names of its
contributors may be used to endorse or promote products
derived from this software without specific prior
written permission of Jacob Oettinger and Joakim Nygård.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
#!/bin/sh
if [ "$1" == "" ]
then
echo "Usage: package.sh <tag>"
else
mkdir package_tmp
rm webgrind-$1.zip
cd package_tmp
svn export https://webgrind.googlecode.com/svn/tags/$1 webgrind
svn export https://webgrind.googlecode.com/svn/wiki webgrind/docs
for i in webgrind/docs/*.wiki; do mv "$i" "${i%.wiki}.txt"; done
rm webgrind/package.sh
zip -r ../webgrind-$1.zip webgrind
cd ..
rm -rf package_tmp
fi
body {
font-family : Helvetica, Verdana, Arial, sans-serif;
font-size : 12px;
color : #000000;
margin:0px;
}
a {
color:#000000;
text-decoration:none;
}
a:hover {
text-decoration:underline;
}
#footer a {
text-decoration:underline;
}
img {
border: 0;
}
h2 {
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;
}
#logo {
float:left;
}
#logo h1 {
font:normal 30px "Futura", Helvetica, Verdana;
padding:0px;
margin:0px;
}
#logo p {
font:normal 11px "Verdana";
margin:0px;
padding:0px;
}
#options {
float:right;
width:580px;
padding:10px 0 0 0;
}
#options form {
margin:0px;
}
#main {
margin:10px;
}
#hello_message {
text-align: center;
margin: 30px 0;
}
#trace_view {
display:none;
}
#runtime_sum,
#invocation_sum,
#shown_sum {
font-weight: bold;
}
#footer {
text-align: center;
font: normal 11px;
}
div.hr {
border-top: 1px solid black;
margin:10px 5px;
}
div.callinfo_area {
display: none;
margin: 5px 5px;
}
table.tablesorter {
border-width: 1px 0 1px 1px;
border-style: solid;
border-color: #D9D9D9;
font-family:arial;
margin:10px 0pt 15px;
padding:0px;
font-size: 8pt;
width: 100%;
text-align: left;
}
table.tablesorter thead tr th, table.tablesorter tfoot tr th {
background-color: #D7DDE4;
border-right: 1px solid #CDCDCD;
border-bottom: 1px solid #CDCDCD;
font-size: 8pt;
padding: 4px;
}
table.tablesorter thead tr .header {
background-image: url(../img/bg.gif);
background-repeat: no-repeat;
background-position: center right;
cursor: pointer;
}
table.tablesorter tbody td {
color: #000000;
border-right:1px solid #D9D9D9;
padding: 4px;
vertical-align: top;
}
table.tablesorter tbody tr.odd {
background-color:#EAEEF2;
}
table.tablesorter tbody tr.even {
background-color:#FFFFFF;
}
table.tablesorter thead tr .headerSortUp {
background-image: url(../img/asc.gif);
}
table.tablesorter thead tr .headerSortDown {
background-image: url(../img/desc.gif);
}
table.tablesorter thead tr .headerSortDown, table.tablesorter thead tr .headerSortUp {
background-color: #9BA8C6;
}
td.nr {
text-align: right;
font-family: "Lucida Console", "Andale Mono", "Monaco", monospace;
}
th span{
margin-right: 13px;
}
img.list_reload {
margin:0px 5px;
}
.block_box {
margin: 10px 10px;
}
.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;
}
a.load_invocations {
display: none;
background-color: #999;
border: 1px solid #333;
padding: 2px;
}
\ No newline at end of file
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<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">
<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');
});
</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>
</body>
</html>
\ No newline at end of file
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<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">
<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>
<script src="js/jquery.selectboxes.js" type="text/javascript" charset="utf-8"></script>
<script src="js/sprintf.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8">
var fileUrlFormat = '<?php echo Webgrind_Config::$fileUrlFormat?>';
var currentDataFile = null;
var callInfoLoaded = new Array();
var disableAjaxBlock = false;
function getOptions(specificFile){
var options = new Object();
options.dataFile = specificFile || $("#dataFile").val();
options.costFormat = $('#costFormat').val();
options.showFraction = $("#showFraction").val();
options.hideInternals = $('#hideInternals').attr('checked') ? 1 : 0;
return options;
}
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;
}
callInfoLoaded = new Array();
$("#function_table tbody").empty();
for(i=0;i<data.functions.length;i++){
callInfoLoaded[data.functions[i].nr] = false;
$("#function_table tbody").append(functionTableRow(data.functions[i], data.linkToFunctionLine));
}
currentDataFile = data.dataFile;
$("#data_file").html(data.dataFile);
$("#invoke_url").html(data.invokeUrl);
$(document).attr('title', 'webgrind of '+data.invokeUrl);
$("#mtime").html(data.mtime);
$("#shown_sum").html(data.functions.length);
$("#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>'
);
$("#hello_message").hide();
$("#trace_view").show();
$("#function_table").trigger('update');
$("#function_table").trigger("sorton",[[[4,1]]]);
$('#callfilter').trigger('keyup');
}
);
}
function showCallGraph() {
vars = getOptions();
vars.op = 'function_graph';
delete vars.costFormat;
delete vars.hideInternals;
window.open('index.php?' + $.param(vars));
}
function reloadFilelist(){
$.getJSON("index.php",
{'op':'file_list'},
function(data){
var options = new Object();
for(i=0;i<data.length;i++){
options[data[i]['filename']] = data[i]['invokeUrl']+' ('+data[i]['filename']+')'+' ['+data[i]['filesize']+']';
}
$("#dataFile").removeOption(/[^0]/);
$("#dataFile").addOption(options, false);
}
);
}
function loadCallInfo(functionNr){
$.getJSON("index.php",
{'op':'callinfo_list', 'file':currentDataFile, 'functionNr':functionNr, 'costFormat':$("#costFormat").val()},
function(data){
if (data.error) {
$("#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)
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
}
}
});
$("#"+idPrefix+functionNr).bind("sortStart",sortBlock).bind("sortEnd",$.unblockUI);
$("#"+idPrefix+functionNr).trigger("sorton",[[[2,1]]]);
}
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> \
</tbody> \
</table> \
';
}
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)
+ ' @ '+data.line+'</td> \
<td class="nr">'+data.callCount+'</td> \
<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 openCallInfo(functionNr) {
var areaEl = $("#callinfo_area_"+functionNr);
if (areaEl.length) {
if (areaEl.is(":hidden")) toggleCallInfo(functionNr);
window.scrollTo(0, areaEl.parent().offset().top);
}
}
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>';;
} 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>';
}
}
return '<tr> \
<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+' \
</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> \
</tr> \
';
}
function sortBlock(){
$.blockUI('<div class="block_box"><h1>Sorting...</h1></div>');
}
function loadBlock(){
if(!disableAjaxBlock)
$.blockUI();
disableAjaxBlock = false;
}
function checkVersion(){
disableAjaxBlock = true;
$.getJSON("index.php",
{'op':'version_info'},
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() {
$.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.fadeIn = 0;
$.blockUI.defaults.fadeOut = 0;
$().ajaxStart(loadBlock).ajaxStop($.unblockUI);
$("#function_table").tablesorter({
widgets: ['zebra'],
sortInitialOrder: 'desc',
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));
}
<?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');
});
});
});
</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><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>
</body>
</html>
\ No newline at end of file
......@@ -44,6 +44,7 @@ $pconfig['noantilockout'] = isset($config['system']['webgui']['noantilockout']);
$pconfig['nodnsrebindcheck'] = isset($config['system']['webgui']['nodnsrebindcheck']);
$pconfig['nohttpreferercheck'] = isset($config['system']['webgui']['nohttpreferercheck']);
$pconfig['beast_protection'] = isset($config['system']['webgui']['beast_protection']);
$pconfig['enable_xdebug'] = isset($config['system']['webgui']['enable_xdebug']) ;
$pconfig['loginautocomplete'] = isset($config['system']['webgui']['loginautocomplete']);
$pconfig['althostnames'] = $config['system']['webgui']['althostnames'];
$pconfig['enableserial'] = $config['system']['enableserial'];
......@@ -167,6 +168,12 @@ if ($_POST) {
else
unset($config['system']['webgui']['beast_protection']);
if ($_POST['enable_xdebug'] == "yes") {
$config['system']['webgui']['enable_xdebug'] = true;
} else {
unset($config['system']['webgui']['enable_xdebug']);
}
if ($_POST['loginautocomplete'] == "yes")
$config['system']['webgui']['loginautocomplete'] = true;
else
......@@ -478,6 +485,17 @@ include("head.inc");
"More information on BEAST is available from <a target='_blank' href='https://en.wikipedia.org/wiki/Transport_Layer_Security#BEAST_attack'>Wikipedia</a>."); ?>
</td>
</tr>
<tr>
<td width="22%" valign="top" class="vncell"><?=gettext("Enable XDebug"); ?></td>
<td width="78%" class="vtable">
<input name="enable_xdebug" type="checkbox" id="enable_xdebug" value="yes" <?php if ($pconfig['enable_xdebug']) echo "checked=\"checked\""; ?> />
<strong><?=gettext("Enable debugger / profiler (developer mode, do not enable in production environment)"); ?></strong>
<br />
<?php echo gettext("When this is checked, php XDebug will be enabled and profiling output can be analysed using webgrind which will be available at [this-url]/webgrind/"); ?>
<br />
<?php echo gettext("For more information about XDebug profiling and how to enable it for your requests, please visit http://www.xdebug.org/docs/all_settings#profiler_enable_trigger"); ?>
</td>
</tr>
<tr>
<th colspan="2" valign="top" class="listtopic"><?=gettext("Secure Shell"); ?></th>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment