Open main menu

Extension:TemplateProfiler

MediaWiki extensions manual
OOjs UI icon advanced.svg
TemplateProfiler
Release status: stable
Implementation Page action
Description Profiles templates and parser function and shows results in a tree and aggregated results in a table. Requires minimal hacking of the parser.
Author(s) Zoran Obradović
Latest version 0.9 (2009-06-21)
MediaWiki 1.14. X
License GPL 3
Download see below
Translate the TemplateProfiler extension if it is available at translatewiki.net
Check usage and version matrix.

PurposeEdit

Profiling templates to make your Wiki faster. Requires minimal hacking of the parser.

UsageEdit

add ?action=profile to the page url

InstallationEdit

  1. Open your includes/parser/Parser.php file.
  2. Find the line that contains # SUBST in the braceSubstitution function.
  3. Insert the following line before that line:
    wfRunHooks( 'BeforeBraceSubstitution', array( &$parser, &$originalTitle, &$args) );
    
  4. Scroll down to the end of the function braceSubstitution. Find the line that contains wfProfileOut( __METHOD__ ); a few lines before the end of the function.
  5. Insert the following line before that line:
    wfRunHooks( 'AfterBraceSubstitution', array( &$parser, &$originalTitle) );
    
  6. Save and close Parser.php
  7. Copy the code into a file named TemplateProfiler.php in your extensions directory.
  8. Copy the css to the end of your Mediawiki:Common.css
  9. Add the following line to the end of your LocalSettings.php file
    require_once('extensions/TemplateProfiler.php');
    

CodeEdit

<?php 

$wgTemplateProfiler = new TemplateProfiler;

class TemplateProfiler
{	
	var $active 		= false;		# for hooks to know whether to collect data or not
	var $data 			= array(null);	# for collecting profiling data
	
	var $outputTree		= array();		# tree of profiling data, for output
	var $outputList 	= array();		# summary list, for output
	var $outputSubList 	= array();		# summary sub list, for output
	var $totalTime  	= 0;			# total time, for use in output
	var $maxNetTime 	= 0;			# maximum net time, for use in output
	
	# arguments:
	# profile_show  : templates, all, or dump, defaults to templates
	# profile_min   : smallest time in ms to include in the list, defaults to 10
	# profile_hl    : total, net or dump, defaults to net
	
	var $show = 'templates';
	var $min  = 10;
	var $hl   = 'net';

	function registerHook( $name )
	{
		global $wgHooks;
	 	$wgHooks[ $name ][] = array( &$this , "hook_$name" );
	}

	function __construct()
	{
		$this->registerHook('UnknownAction');
		$this->registerHook('BeforeBraceSubstitution');
		$this->registerHook('AfterBraceSubstitution');
	}
	
################################
#
#   M A I N   F U N C T I O N
#
################################

	function hook_UnknownAction($action, &$article)
	{
#		die ($action);
		if ($action!='profile') return true;
		global $wgRequest, $wgOut;
		
		# gather and fix parameters
		$this->min   = (int) $wgRequest->getVal('profile_min','10');
		if ($this->min < 0) $this->min = 0;

		switch ($this->show = $wgRequest->getText('profile_show')) {
			case 'all':	case 'dump': break;
			default: $show='templates';
		}
		
		switch ($this->hl = $wgRequest->getText('profile_hl')) {
			case 'dump': case 'total': break;
			default: $this->hl='net';
		}

		#collect profiling data for this article
		$this->collectData($article);

		# if we're just dumping, return print_r 
		if ($this->show=='dump')
		{
			$wgOut->addHTML("<pre>".print_r($this->data,true)."</pre>");
			return false; 
		}
		
		# otherwise, we need to extract data for display
		$this->processData();
		# output the results
		$wgOut->addHTML('<div id="template-profiler">');
		$this->outputHeader($article->getTitle());
		$this->outputData();
		$wgOut->addHTML('</div>');
		return false;
	}

################################
#
#   D A T A   C O L L E C T I O N
#
################################

	# profiles an article, by using hooks to collect times on braceSubstitution
	function collectData(&$article)
	{
		global $wgParser, $wgUser, $wgRequest;

		#get title for later use
		$title = $article->getTitle();

		# prepare root node
		$this->data = array
		(
			'parent' => null,
			'title' => ':'.$title->getFullText(),
			'children' => array()
		);

		# prepare the parser
		$parser =& $wgParser;
		$this->active = true;
		$options = ParserOptions::newFromUser($wgUser);

		#get text
		$text = $article->getContent();

		#parse the page, recording times for the root node 		
		$this->data['start']= microtime(true);
		$parser->parse($article->getContent(), $title, $options, false);		
		$this->data['end']= microtime(true);
	}


	function hook_BeforeBraceSubstitution(&$parser,$titleText,&$args)
	{
		# work only if active
		if (!$this->active) return true;
		$node = array
		(
			'parent' => &$this->data,
			'start' => microtime(true),
			'title' => $titleText,
			'children' => array()
		);

		$this->data['children'][] =& $node;
		$this->data =& $node;
		
		return true;
	}
	
	function hook_AfterBraceSubstitution(&$parser,$titleText)
	{
		if (!$this->active) return true;
		$end = microtime(true);
		$this->data['end']= $end;
		$this->data =& $this->data['parent'];
		return true;
	}


###################################
#
#   D A T A   P R O C E S S I N G
#
###################################

	# process collected data, producing $this->outputList and $this->outputTree
	function processData(&$node=null)
	{
		if ($node===null)
		{
			$initial=true;
			$this->outputList=array();
			$this->outputSubList=array();
			$this->maxNetTime=0;
			$theNode =& $this->data;
			$this->totalTime = $this->data['end']-$this->data['start'];
		}
		else
		{
			$initial=false;
			$theNode =& $node;
		}
		
		$newNode = array
		(
			'title'    => $theNode['title'],
			'children' => array(),
			'time'     => $theNode['end'] - $theNode['start'],
			'nettime'  => $theNode['end'] - $theNode['start']
		);
		foreach ($theNode['children'] as &$child)
		{
			$childTime = $child['end'] - $child['start'];
			
			if ($this->show='all' || $this->isTemplate($child['title']))
			{
				$childNode = $this->processData($child);
				
				if ($childTime >= $this->min/1000)
				{
					
					$newNode['children'][] = $childNode;
					$newNode['nettime'] -= $childTime;
				}
				else
				{
					$newNode['nettime'] -= $childTime;
				    $newNode['children'][-1]['nettime']+=$childTime; 
				    $newNode['children'][-1]['time']+=$childTime; 
				    $newNode['children'][-1]['title']="{other}"; 
				}
				
				list ($group,$item)=$this->splitTitle($child['title']);
#				print ("$group,$item," . $childTime - $childNode['nettime'] . "<br>");
				
				$this->outputSubList[$group][$item]['time']+=$childTime;
				$this->outputSubList[$group][$item]['nettime']+=$childNode['nettime'];
				$this->outputSubList[$group][$item]['count']++;
				$this->outputList[$group]['time']+=$childTime;
				$this->outputList[$group]['nettime']+=$childNode['nettime'];
				$this->outputList[$group]['count']++;
				if ($childNode['nettime'] > $this->maxNetTime) $this->maxNetTime = $childNode['nettime'];
			}
		}
		if (!$initial) return $newNode;
		$this->outputTree=$newNode;
		return true;
	}

	# is the title a variable?
	function isVariable($titleText)
	{
		global $wgParser;
		return $wgParser->mVariables->matchStartToEnd($titleText);
	}

	# is the title a function?
	function isFunction($titleText)
	{
		global $wgParser;
		$parts = split(':',$titleText,2);
		return	(
			count($parts) == 2 
			&&	(
				isset( $wgParser->mFunctionSynonyms[0][strtolower($parts[0])] ) 
				||	
				isset( $wgParser->mFunctionSynonyms[1][$parts[0]] )
			)
		);
	}

	# is the title a template?
	function isTemplate($titleText)
	{
		return !($this->isVariable($titleText) || $this->isFunction($titleText) || !Title::newFromText($titleText));
	}

	# split 
	function splitTitle($titleText)
	{
		if ($this->isVariable($titleText)) return array ('variables',$titleText);

		if ($this->isFunction($titleText))
		{
			return split(':',$titleText,2);
		}
		
		$title = Title::newFromText($titleText,NS_TEMPLATE);
		return array($title->getNsText(),$title->getText());
	}

###################################
#
#   O U T P U T
#
###################################

	function outputHeader($title)
	{
		global $wgOut;
		
		$wgOut->addHTML
		(
			'show: <a href="' . $title->getFullUrl('action=profile&profile_min=0') . '">all</a>'
			.' &middot; <a href="' . $title->getFullUrl('action=profile&profile_min=10') . '">over 10 ms</a>'
			.' &middot; <a href="' . $title->getFullUrl('action=profile&profile_min=100') . '">over 100 ms</a>'
		);
	}
	
	function outputData()
	{
		global $wgOut;

		if ($this->hl=='dump')
		{
			$wgOut->addHTML("<pre>".print_r(array('tree'=>$this->outputTree,'list'=>$this->outputList),true)."</pre>");
			return; 
		}
		$this->outputTreeData();
		$this->outputListData();
	}
	
	function outputTreeData()
	{
		global $wgOut;

		$text = $this->outputTreeNode($this->outputTree);
		$wgOut->addHTML('<ul>');
		$wgOut->addWikiText($text);
		$wgOut->addHTML('</ul></div>');
		return false;
	}
	

	function outputTreeNode(&$node)
	{
		$time = $node['time'];
		$nettime = $node['nettime'];
		
		$shade = (int)($nettime*1000);
		if ($shade>50) $shade=50;
		$shade = (50-$shade)/5;
		$shade=$shade*$shade;
		$shade = (int)(100-$shade);
		$color = "rgb($shade,$shade,0)";
						
		$ret = '<li style="background:'.$color.'" class="profiler-treeitem">';
		
		$ret .= '<span class="profiler-time">' . ($time<.001 ? (int)($time*10000)/10 : (int)($time*1000)) .'</span>';
		$ret.= '<span class="profiler-nettime">' . ($nettime<.001 ? (int)($nettime*10000)/10 : (int)($nettime*1000)) .'</span>';
		$ret.= '<span class="profiler-title">';
		$ret.= $this->getTitle($node['title']);
		$ret.='</span>';
		if (count($node['children']))
		{
			$ret.='<ul>';
			foreach ($node['children'] as &$child)
			{
				$ret.=$this->outputTreeNode(&$child,$minTime);
			}
			$ret.='</ul>';
		}
		return $ret;
	}

	function compareNetTime(&$l,&$r)
	{
		if ($l['nettime']>$r['nettime']) return -1;
		elseif ($l['nettime']<$r['nettime']) return 1;
		else return 0;
	}

	function outputListData()
	{
		global $wgOut;
		$wgOut->addHTML("<table id=\"profiler-list\"><tr id=\"profiler-listheader\"><th>group / item</th><th>count</th><th>net time (ms)</th><th>total time (ms)</th></th></tr>");
		uasort($this->outputList,array($this,'compareNetTime'));
		foreach ($this->outputList as $groupName=>$group)
		{
			if ($group['time']>=$this->min/1000)
			{
				$wgOut->addHTML("<tr class=\"profiler-listgroup\"><th>".($groupName?$groupName:'(main)').":</th><td>${group['count']}</td><td>"
								.(int)($group['nettime']*1000)
								."</td><td>"
								.(int)($group['time']*1000)
								."</td></tr>"
								);
				uasort($this->outputSubList[$groupName],array($this,'compareNetTime'));
				foreach ($this->outputSubList[$groupName] as $itemName=>$item)
				{
					if ($item['time']>=$this->min/1000)
					{
						$wgOut->addHTML("<tr class=\"profiler-listitem\"><th>$itemName</th><td>${item['count']}</td><td>"
										.(int)($item['nettime']*1000)
										."</td><td>"
										.(int)($item['time']*1000)
										."</td></tr>");	
					}
				}
			}
		}
		$wgOut->addHTML("</table>");
		return; 
	}

	function getTitle($titleText)
	{
		if (!$this->IsTemplate($titleText)) return $titleText;
		$title = Title::newFromText($titleText,NS_TEMPLATE)->getFullText();
		return "[[$title]]";
	}
}

CssEdit

/* ### PROFILER ### */

#template-profiler,
.box {
 margin:0 0 10px 0;
 padding:10px;
 background:#272727;
 color:white;
}

#template-profiler a
{
  color:orange;
}

#template-profiler ul
{
  margin:0;
  padding:0;
}

#profiler-list
{
  background:black;
  width:100%;
  color:white;
  border-collapse:collapse;
}

#profiler-list th,
#profiler-list td
{
  border-top:dotted 1px #022;
  border-bottom:dotted 1px #022;
}
#profiler-list td
{
  text-align:right;
  font-size:90%;
}
tr.profiler-listgroup td
{
  color:red;
  background:#111;
  width:6em;
  text-align:right;
  color:white;
}

.profiler-listgroup th
{
  text-align:left;
  background:#111;
}


.profiler-listitem th
{
  text-align:left;
  font-weight:normal;
  padding-left:2em;
}

.profiler-treeitem
{
  list-style-type:none;
  marker-offset:0px;
  padding:0px 0px 0px 24px;
  margin:0px -1px -1px 0px;
  border:dotted 1px #033;
}

.profiler-time
{
  font-size:10px;
  float:left;
  height:100%;
  margin-left:-24px;
  width:24px;
  padding-right:6px;
  text-align:right;
  color:#8ff;
}

.profiler-nettime
{
  font-size:10px;
  width:22px;
  float:right;
  text-align:right;
  padding-right:2px;
  color:#8ff;
}