Add limit report data on preview pages
authorBrad Jorsch <bjorsch@wikimedia.org>
Thu, 14 Mar 2013 12:43:06 +0000 (08:43 -0400)
committerTim Starling <tstarling@wikimedia.org>
Tue, 13 Aug 2013 05:54:03 +0000 (05:54 +0000)
While we've long had the "NewPP limit report" hidden in an HTML comment,
it is hard for users to find this as they're not likely to look for
profiling information hidden in an HTML comment. Even for those aware of
it, it's not particularly convenient to find.

This changeset adds a table showing this information at the bottom of
the page preview. It also adds the ability for this information to be
added to the ParserOutput object in a structured manner, and various
messages so the report can be localized for the end user.

Note that, for backwards compatability, the default English messages are
used for the "NewPP limit report" comment rather than the localized
messages.

Change-Id: Ie065c7b5a17bbf1aa484d0ae1f3ee0f5d41f8495

RELEASE-NOTES-1.22
docs/hooks.txt
includes/EditPage.php
includes/parser/Parser.php
includes/parser/ParserOutput.php
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.php
maintenance/language/messages.inc
resources/mediawiki.action/mediawiki.action.edit.collapsibleFooter.css
resources/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js

index af467a0..b9890a0 100644 (file)
@@ -121,6 +121,10 @@ production.
 * (bug 43689) The lists of templates used on the page and hidden categories it
   is a member of, shown below the edit form, are now collapsible (and collapsed
   by default).
+* Parser profiling data, formerly only available in the "NewPP limit report"
+  HTML comment, is now also displayed at the bottom of page previews.
+* Added ParserLimitReportPrepare and ParserLimitReportFormat hooks, deprecated
+  ParserLimitReport hook.
 * New user rights have been added to increase granularity in rights management
   for extensions such as OAuth:
 ** editmyusercss controls whether a user may edit their own CSS subpages.
index 23ed032..50b97a3 100644 (file)
@@ -1802,10 +1802,29 @@ cache or return false to not use it.
 $parser: Parser object
 $varCache: variable cache (array)
 
-'ParserLimitReport': Called at the end of Parser:parse() when the parser will
+'ParserLimitReport': DEPRECATED, use ParserLimitReportPrepare and
+ParserLimitReportFormat instead.
+Called at the end of Parser:parse() when the parser will
 include comments about size of the text parsed.
 $parser: Parser object
-$limitReport: text that will be included (without comment tags)
+&$limitReport: text that will be included (without comment tags)
+
+'ParserLimitReportFormat': Called for each row in the parser limit report that
+needs formatting. If nothing handles this hook, the default is to use "$key" to
+get the label, and "$key-value" or "$key-value-text"/"$key-value-html" to
+format the value.
+$key: Key for the limit report item (string)
+$value: Value of the limit report item
+&$report: String onto which to append the data
+$isHTML: If true, $report is an HTML table with two columns; if false, it's
+       text intended for display in a monospaced font.
+$localize: If false, $report should be output in English.
+
+'ParserLimitReportPrepare': Called at the end of Parser:parse() when the parser will
+include comments about size of the text parsed. Hooks should use
+$output->setLimitReportData() to populate data.
+$parser: Parser object
+$output: ParserOutput object
 
 'ParserMakeImageParams': Called before the parser make an image link, use this
 to modify the parameters of the image.
index 17a1946..fe1ca00 100644 (file)
@@ -2279,6 +2279,9 @@ class EditPage {
                $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'hiddencats' ),
                        Linker::formatHiddenCategories( $this->mArticle->getHiddenCategories() ) ) );
 
+               $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'limitreport' ),
+                       self::getPreviewLimitReport( $this->mParserOutput ) ) );
+
                $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
 
                if ( $this->isConflict ) {
@@ -2858,6 +2861,59 @@ HTML
                        call_user_func_array( 'wfMessage', $copywarnMsg )->plain() . "\n</div>";
        }
 
+       /**
+        * Get the Limit report for page previews
+        *
+        * @since 1.22
+        * @param ParserOutput $output ParserOutput object from the parse
+        * @return string HTML
+        */
+       public static function getPreviewLimitReport( $output ) {
+               if ( !$output || !$output->getLimitReportData() ) {
+                       return '';
+               }
+
+               wfProfileIn( __METHOD__ );
+
+               $limitReport = Html::rawElement( 'div', array( 'class' => 'mw-limitReportExplanation' ),
+                       wfMessage( 'limitreport-title' )->parseAsBlock()
+               );
+
+               // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
+               $limitReport .= Html::openElement( 'div', array( 'class' => 'preview-limit-report-wrapper' ) );
+
+               $limitReport .= Html::openElement( 'table', array(
+                       'class' => 'preview-limit-report wikitable'
+               ) ) .
+                       Html::openElement( 'tbody' );
+
+               foreach ( $output->getLimitReportData() as $key => $value ) {
+                       if ( wfRunHooks( 'ParserLimitReportFormat',
+                               array( $key, $value, &$limitReport, true, true )
+                       ) ) {
+                               $keyMsg = wfMessage( $key );
+                               $valueMsg = wfMessage( array( "$key-value-html", "$key-value" ) );
+                               if ( !$valueMsg->exists() ) {
+                                       $valueMsg = new RawMessage( '$1' );
+                               }
+                               if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
+                                       $limitReport .= Html::openElement( 'tr' ) .
+                                               Html::rawElement( 'th', null, $keyMsg->parse() ) .
+                                               Html::rawElement( 'td', null, $valueMsg->params( $value )->parse() ) .
+                                               Html::closeElement( 'tr' );
+                               }
+                       }
+               }
+
+               $limitReport .= Html::closeElement( 'tbody' ) .
+                       Html::closeElement( 'table' ) .
+                       Html::closeElement( 'div' );
+
+               wfProfileOut( __METHOD__ );
+
+               return $limitReport;
+       }
+
        protected function showStandardInputs( &$tabindex = 2 ) {
                global $wgOut;
                $wgOut->addHTML( "<div class='editOptions'>\n" );
index 4a16f56..1bba876 100644 (file)
@@ -362,6 +362,9 @@ class Parser {
                $this->startParse( $title, $options, self::OT_HTML, $clearState );
 
                $this->mInputSize = strlen( $text );
+               if ( $this->mOptions->getEnableLimitReport() ) {
+                       $this->mOutput->resetParseStartTime();
+               }
 
                # Remove the strip marker tag prefix from the input, if present.
                if ( $clearState ) {
@@ -492,22 +495,64 @@ class Parser {
                # Information on include size limits, for the benefit of users who try to skirt them
                if ( $this->mOptions->getEnableLimitReport() ) {
                        $max = $this->mOptions->getMaxIncludeSize();
-                       $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/{$this->mOptions->getExpensiveParserFunctionLimit()}\n";
-                       $limitReport =
-                               "NewPP limit report\n" .
-                               "Preprocessor visited node count: {$this->mPPNodeCount}/{$this->mOptions->getMaxPPNodeCount()}\n" .
-                               "Preprocessor generated node count: " .
-                                       "{$this->mGeneratedPPNodeCount}/{$this->mOptions->getMaxGeneratedPPNodeCount()}\n" .
-                               "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
-                               "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n" .
-                               "Highest expansion depth: {$this->mHighestExpansionDepth}/{$this->mOptions->getMaxPPExpandDepth()}\n" .
-                               $PFreport;
+
+                       $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
+                       if ( $cpuTime !== null ) {
+                               $this->mOutput->setLimitReportData( 'limitreport-cputime',
+                                       sprintf( "%.3f", $cpuTime )
+                               );
+                       }
+
+                       $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
+                       $this->mOutput->setLimitReportData( 'limitreport-walltime',
+                               sprintf( "%.3f", $wallTime )
+                       );
+
+                       $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
+                               array( $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() )
+                       );
+                       $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
+                               array( $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() )
+                       );
+                       $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
+                               array( $this->mIncludeSizes['post-expand'], $max )
+                       );
+                       $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
+                               array( $this->mIncludeSizes['arg'], $max )
+                       );
+                       $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
+                               array( $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() )
+                       );
+                       $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
+                               array( $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() )
+                       );
+                       wfRunHooks( 'ParserLimitReportPrepare', array( $this, $this->mOutput ) );
+
+                       $limitReport = "NewPP limit report\n";
+                       foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
+                               if ( wfRunHooks( 'ParserLimitReportFormat',
+                                       array( $key, $value, &$limitReport, false, false )
+                               ) ) {
+                                       $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
+                                       $valueMsg = wfMessage( array( "$key-value-text", "$key-value" ) )
+                                               ->inLanguage( 'en' )->useDatabase( false );
+                                       if ( !$valueMsg->exists() ) {
+                                               $valueMsg = new RawMessage( '$1' );
+                                       }
+                                       if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
+                                               $valueMsg->params( $value );
+                                               $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
+                                       }
+                               }
+                       }
+                       // Since we're not really outputting HTML, decode the entities and
+                       // then re-encode the things that need hiding inside HTML comments.
+                       $limitReport = htmlspecialchars_decode( $limitReport );
                        wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
 
                        // Sanitize for comment. Note '‐' in the replacement is U+2010,
                        // which looks much like the problematic '-'.
                        $limitReport = str_replace( array( '-', '&' ), array( '‐', '&amp;' ), $limitReport );
-
                        $text .= "\n<!-- \n$limitReport-->\n";
 
                        if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
index c5e42a4..5cb70cb 100644 (file)
@@ -52,6 +52,8 @@ class ParserOutput extends CacheTime {
                private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys)
                private $mSecondaryDataUpdates = array(); # List of DataUpdate, used to save info from the page somewhere else.
                private $mExtensionData = array(); # extra data used by extensions
+               private $mLimitReportData = array(); # Parser limit report data
+               private $mParseStartTime = array(); # Timestamps for getTimeSinceStart()
 
        const EDITSECTION_REGEX = '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)(</(?:mw:)?editsection>))#';
 
@@ -120,6 +122,7 @@ class ParserOutput extends CacheTime {
        function getIndexPolicy()            { return $this->mIndexPolicy; }
        function getTOCHTML()                { return $this->mTOCHTML; }
        function getTimestamp()              { return $this->mTimestamp; }
+       function getLimitReportData()        { return $this->mLimitReportData; }
 
        function setText( $text )            { return wfSetVar( $this->mText, $text ); }
        function setLanguageLinks( $ll )     { return wfSetVar( $this->mLanguageLinks, $ll ); }
@@ -544,4 +547,67 @@ class ParserOutput extends CacheTime {
                return null;
        }
 
+       private static function getTimes( $clock = null ) {
+               $ret = array();
+               if ( !$clock || $clock === 'wall' ) {
+                       $ret['wall'] = microtime( true );
+               }
+               if ( ( !$clock || $clock === 'cpu' ) && function_exists( 'getrusage' ) ) {
+                       $ru = getrusage();
+                       $ret['cpu'] = $ru['ru_utime.tv_sec'] + $ru['ru_utime.tv_usec'] / 1e6;
+                       $ret['cpu'] += $ru['ru_stime.tv_sec'] + $ru['ru_stime.tv_usec'] / 1e6;
+               }
+               return $ret;
+       }
+
+       /**
+        * Resets the parse start timestamps for future calls to getTimeSinceStart()
+        * @since 1.22
+        */
+       function resetParseStartTime() {
+               $this->mParseStartTime = self::getTimes();
+       }
+
+       /**
+        * Returns the time since resetParseStartTime() was last called
+        *
+        * Clocks available are:
+        *  - wall: Wall clock time
+        *  - cpu: CPU time (requires getrusage)
+        *
+        * @since 1.22
+        * @param string $clock
+        * @return float|null
+        */
+       function getTimeSinceStart( $clock ) {
+               if ( !isset( $this->mParseStartTime[$clock] ) ) {
+                       return null;
+               }
+
+               $end = self::getTimes( $clock );
+               return $end[$clock] - $this->mParseStartTime[$clock];
+       }
+
+       /**
+        * Sets parser limit report data for a key
+        *
+        * The key is used as the prefix for various messages used for formatting:
+        *  - $key: The label for the field in the limit report
+        *  - $key-value-text: Message used to format the value in the "NewPP limit
+        *      report" HTML comment. If missing, uses $key-format.
+        *  - $key-value-html: Message used to format the value in the preview
+        *      limit report table. If missing, uses $key-format.
+        *  - $key-value: Message used to format the value. If missing, uses "$1".
+        *
+        * Note that all values are interpreted as wikitext, and so should be
+        * encoded with htmlspecialchars() as necessary, but should avoid complex
+        * HTML for sanity of display in the "NewPP limit report" comment.
+        *
+        * @since 1.22
+        * @param string $key Message key
+        * @param mixed $value Appropriate for Message::params()
+        */
+       function setLimitReportData( $key, $value ) {
+               $this->mLimitReportData[$key] = $value;
+       }
 }
index 57e2cc3..b60ff6e 100644 (file)
@@ -5118,4 +5118,23 @@ Otherwise, you can use the easy form below. Your comment will be added to the pa
 # Image rotation
 'rotate-comment' => 'Image rotated by $1 {{PLURAL:$1|degree|degrees}} clockwise',
 
+# Limit report
+'limitreport-title' => 'Parser profiling data:',
+'limitreport-cputime' => 'CPU time usage',
+'limitreport-cputime-value' => '$1 {{PLURAL:$1|second|seconds}}',
+'limitreport-walltime' => 'Real time usage',
+'limitreport-walltime-value' => '$1 {{PLURAL:$1|second|seconds}}',
+'limitreport-ppvisitednodes' => 'Preprocessor visited node count',
+'limitreport-ppvisitednodes-value' => '$1/$2',
+'limitreport-ppgeneratednodes' => 'Preprocessor generated node count',
+'limitreport-ppgeneratednodes-value' => '$1/$2',
+'limitreport-postexpandincludesize' => 'Post-expand include size',
+'limitreport-postexpandincludesize-value' => '$1/$2 bytes',
+'limitreport-templateargumentsize' => 'Template argument size',
+'limitreport-templateargumentsize-value' => '$1/$2 bytes',
+'limitreport-expansiondepth' => 'Highest expansion depth',
+'limitreport-expansiondepth-value' => '$1/$2',
+'limitreport-expensivefunctioncount' => 'Expensive parser function count',
+'limitreport-expensivefunctioncount-value' => '$1/$2',
+
 );
index 5dbdbeb..62b50d4 100644 (file)
@@ -9266,4 +9266,37 @@ Parameters:
 # Image rotation
 'rotate-comment' => 'Edit summary for the act of rotating an image.',
 
+# Limit report
+'limitreport-title' => 'Title for the preview limit report table.',
+'limitreport-cputime' => 'Label for the "CPU time usage" row in the limit report table',
+'limitreport-cputime-value' => 'Format for the "CPU time usage" value in the limit report table.
+* $1 is the time usage in seconds',
+'limitreport-walltime' => 'Label for the "Real time usage" row in the limit report table',
+'limitreport-walltime-value' => 'Format for the "Real time usage" value in the limit report table.
+* $1 is the time usage in seconds',
+'limitreport-ppvisitednodes' => 'Label for the "Preprocessor visited node count" row in the limit report table',
+'limitreport-ppvisitednodes-value' => 'Format for the "Preprocessor visited node count" row in the limit report table.
+* $1 is the usage
+* $2 is the maximum',
+'limitreport-ppgeneratednodes' => 'Label for the "Preprocessor generated node count" row in the limit report table',
+'limitreport-ppgeneratednodes-value' => 'Format for the "Preprocessor generated node count" row in the limit report table.
+* $1 is the usage
+* $2 is the maximum',
+'limitreport-postexpandincludesize' => 'Label for the "Post-expand include size" row in the limit report table',
+'limitreport-postexpandincludesize-value' => 'Format for the "Post-expand include size" row in the limit report table.
+* $1 is the usage in bytes
+* $2 is the maximum',
+'limitreport-templateargumentsize' => 'Label for the "Template argument size" row in the limit report table',
+'limitreport-templateargumentsize-value' => 'Format for the "Template argument size" row in the limit report table.
+* $1 is the usage in bytes
+* $2 is the maximum',
+'limitreport-expansiondepth' => 'Label for the "Highest expansion depth" row in the limit report table',
+'limitreport-expansiondepth-value' => 'Format for the "Highest expansion depth" row in the limit report table.
+* $1 is the depth
+* $2 is the maximum',
+'limitreport-expensivefunctioncount' => 'Label for the "Expensive parser function count" row in the limit report table',
+'limitreport-expensivefunctioncount-value' => 'Format for the "Expensive parser function count" row in the limit report table.
+* $1 is the usage
+* $2 is the maximum',
+
 );
index aa9fa9e..c266f89 100644 (file)
@@ -3954,6 +3954,25 @@ $wgMessageStructure = array(
        'rotation' => array(
                'rotate-comment',
        ),
+       'limitreport' => array(
+               'limitreport-title',
+               'limitreport-cputime',
+               'limitreport-cputime-value',
+               'limitreport-walltime',
+               'limitreport-walltime-value',
+               'limitreport-ppvisitednodes',
+               'limitreport-ppvisitednodes-value',
+               'limitreport-ppgeneratednodes',
+               'limitreport-ppgeneratednodes-value',
+               'limitreport-postexpandincludesize',
+               'limitreport-postexpandincludesize-value',
+               'limitreport-templateargumentsize',
+               'limitreport-templateargumentsize-value',
+               'limitreport-expansiondepth',
+               'limitreport-expansiondepth-value',
+               'limitreport-expensivefunctioncount',
+               'limitreport-expensivefunctioncount-value',
+       ),
 );
 
 /** Comments for each block */
@@ -4198,4 +4217,5 @@ Variants for Chinese language",
        'duration'              => 'Durations',
        'cachedspecial'         => 'SpecialCachedPage',
        'rotation'              => 'Image rotation',
+       'limitreport'           => 'Limit report',
 );
index 89f54c4..1af4a7a 100644 (file)
@@ -9,3 +9,9 @@
        margin-bottom: 1em;
        margin-left: 2.5em;
 }
+
+/* Show/hide animation is incorrect if the table has a margin set. Extra
+ * "table.wikitable" is needed in the selector for CSS specificity. */
+table.wikitable.preview-limit-report {
+       margin: 0;
+}
index 0fb5912..7ae51ab 100644 (file)
@@ -12,6 +12,11 @@ jQuery( document ).ready( function ( $ ) {
                        $list: $( '.hiddencats ul' ),
                        $toggler: $( '.mw-hiddenCategoriesExplanation' ),
                        cookieName: 'hidden-categories-list'
+               },
+               {
+                       $list: $( '.preview-limit-report-wrapper' ),
+                       $toggler: $( '.mw-limitReportExplanation' ),
+                       cookieName: 'preview-limit-report'
                }
        ];