Extension:TemplateProfiler

From MediaWiki.org
Jump to: navigation, search
MediaWiki extensions manual - list
Crystal Clear action run.png
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ć
Last version 0.9 (2009-06-21)
MediaWiki 1.14. X
License GPL 3
Download see below
Check usage and version matrix

Contents

Purpose [edit]

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

Usage [edit]

add ?action=profile to the page url

Installation [edit]

  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');
    

Code [edit]

<?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]]";
        }
}

Css [edit]

/* ### 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;
}