1<?php 2/** 3 * Benchmark script for parse operations 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @author Tim Starling <tstarling@wikimedia.org> 22 * @ingroup Benchmark 23 */ 24 25require_once __DIR__ . '/../Maintenance.php'; 26 27use MediaWiki\Linker\LinkTarget; 28use MediaWiki\MediaWikiServices; 29use MediaWiki\Revision\RevisionRecord; 30use MediaWiki\Revision\SlotRecord; 31 32/** 33 * Maintenance script to benchmark how long it takes to parse a given title at an optionally 34 * specified timestamp 35 * 36 * @since 1.23 37 */ 38class BenchmarkParse extends Maintenance { 39 /** @var string MediaWiki concatenated string timestamp (YYYYMMDDHHMMSS) */ 40 private $templateTimestamp = null; 41 42 private $clearLinkCache = false; 43 44 /** 45 * @var LinkCache 46 */ 47 private $linkCache; 48 49 /** @var array Cache that maps a Title DB key to revision ID for the requested timestamp */ 50 private $idCache = []; 51 52 public function __construct() { 53 parent::__construct(); 54 $this->addDescription( 'Benchmark parse operation' ); 55 $this->addArg( 'title', 'The name of the page to parse' ); 56 $this->addOption( 'warmup', 'Repeat the parse operation this number of times to warm the cache', 57 false, true ); 58 $this->addOption( 'loops', 'Number of times to repeat parse operation post-warmup', 59 false, true ); 60 $this->addOption( 'page-time', 61 'Use the version of the page which was current at the given time', 62 false, true ); 63 $this->addOption( 'tpl-time', 64 'Use templates which were current at the given time (except that moves and ' . 65 'deletes are not handled properly)', 66 false, true ); 67 $this->addOption( 'reset-linkcache', 'Reset the LinkCache after every parse.', 68 false, false ); 69 } 70 71 public function execute() { 72 if ( $this->hasOption( 'tpl-time' ) ) { 73 $this->templateTimestamp = wfTimestamp( TS_MW, strtotime( $this->getOption( 'tpl-time' ) ) ); 74 Hooks::register( 'BeforeParserFetchTemplateRevisionRecord', [ $this, 'onFetchTemplate' ] ); 75 } 76 77 $this->clearLinkCache = $this->hasOption( 'reset-linkcache' ); 78 // Set as a member variable to avoid function calls when we're timing the parse 79 $this->linkCache = MediaWikiServices::getInstance()->getLinkCache(); 80 81 $title = Title::newFromText( $this->getArg( 0 ) ); 82 if ( !$title ) { 83 $this->fatalError( "Invalid title" ); 84 } 85 86 $revLookup = MediaWikiServices::getInstance()->getRevisionLookup(); 87 if ( $this->hasOption( 'page-time' ) ) { 88 $pageTimestamp = wfTimestamp( TS_MW, strtotime( $this->getOption( 'page-time' ) ) ); 89 $id = $this->getRevIdForTime( $title, $pageTimestamp ); 90 if ( !$id ) { 91 $this->fatalError( "The page did not exist at that time" ); 92 } 93 94 $revision = $revLookup->getRevisionById( $id ); 95 } else { 96 $revision = $revLookup->getRevisionByTitle( $title ); 97 } 98 99 if ( !$revision ) { 100 $this->fatalError( "Unable to load revision, incorrect title?" ); 101 } 102 103 $warmup = $this->getOption( 'warmup', 1 ); 104 for ( $i = 0; $i < $warmup; $i++ ) { 105 $this->runParser( $revision ); 106 } 107 108 $loops = $this->getOption( 'loops', 1 ); 109 if ( $loops < 1 ) { 110 $this->fatalError( 'Invalid number of loops specified' ); 111 } 112 $startUsage = getrusage(); 113 $startTime = microtime( true ); 114 for ( $i = 0; $i < $loops; $i++ ) { 115 $this->runParser( $revision ); 116 } 117 $endUsage = getrusage(); 118 $endTime = microtime( true ); 119 120 printf( "CPU time = %.3f s, wall clock time = %.3f s\n", 121 // CPU time 122 ( $endUsage['ru_utime.tv_sec'] + $endUsage['ru_utime.tv_usec'] * 1e-6 123 - $startUsage['ru_utime.tv_sec'] - $startUsage['ru_utime.tv_usec'] * 1e-6 ) / $loops, 124 // Wall clock time 125 ( $endTime - $startTime ) / $loops 126 ); 127 } 128 129 /** 130 * Fetch the ID of the revision of a Title that occurred 131 * 132 * @param Title $title 133 * @param string $timestamp 134 * @return bool|string Revision ID, or false if not found or error 135 */ 136 private function getRevIdForTime( Title $title, $timestamp ) { 137 $dbr = $this->getDB( DB_REPLICA ); 138 139 $id = $dbr->selectField( 140 [ 'revision', 'page' ], 141 'rev_id', 142 [ 143 'page_namespace' => $title->getNamespace(), 144 'page_title' => $title->getDBkey(), 145 'rev_timestamp <= ' . $dbr->addQuotes( $timestamp ) 146 ], 147 __METHOD__, 148 [ 'ORDER BY' => 'rev_timestamp DESC' ], 149 [ 'revision' => [ 'JOIN', 'rev_page=page_id' ] ] 150 ); 151 152 return $id; 153 } 154 155 /** 156 * Parse the text from a given RevisionRecord 157 * 158 * @param RevisionRecord $revision 159 */ 160 private function runParser( RevisionRecord $revision ) { 161 $content = $revision->getContent( SlotRecord::MAIN ); 162 $title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() ); 163 164 $content->getParserOutput( $title, $revision->getId() ); 165 if ( $this->clearLinkCache ) { 166 $this->linkCache->clear(); 167 } 168 } 169 170 /** 171 * Hook into the parser's revision ID fetcher. Make sure that the parser only 172 * uses revisions around the specified timestamp. 173 * 174 * @param ?LinkTarget $contextTitle 175 * @param LinkTarget $titleTarget 176 * @param bool &$skip 177 * @param ?RevisionRecord &$revRecord 178 * @return bool 179 */ 180 private function onFetchTemplate( 181 ?LinkTarget $contextTitle, 182 LinkTarget $titleTarget, 183 bool &$skip, 184 ?RevisionRecord &$revRecord 185 ): bool { 186 $title = Title::castFromLinkTarget( $titleTarget ); 187 188 $pdbk = $title->getPrefixedDBkey(); 189 if ( !isset( $this->idCache[$pdbk] ) ) { 190 $proposedId = $this->getRevIdForTime( $title, $this->templateTimestamp ); 191 $this->idCache[$pdbk] = $proposedId; 192 } 193 if ( $this->idCache[$pdbk] !== false ) { 194 $revLookup = MediaWikiServices::getInstance()->getRevisionLookup(); 195 $revRecord = $revLookup->getRevisionById( $this->idCache[$pdbk] ); 196 } 197 198 return true; 199 } 200} 201 202$maintClass = BenchmarkParse::class; 203require_once RUN_MAINTENANCE_IF_MAIN; 204