benchmarkParse.php 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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. require __DIR__ . '/../Maintenance.php';
  25. use MediaWiki\MediaWikiServices;
  26. /**
  27. * Maintenance script to benchmark how long it takes to parse a given title at an optionally
  28. * specified timestamp
  29. *
  30. * @since 1.23
  31. */
  32. class BenchmarkParse extends Maintenance {
  33. /** @var string MediaWiki concatenated string timestamp (YYYYMMDDHHMMSS) */
  34. private $templateTimestamp = null;
  35. private $clearLinkCache = false;
  36. /**
  37. * @var LinkCache
  38. */
  39. private $linkCache;
  40. /** @var array Cache that maps a Title DB key to revision ID for the requested timestamp */
  41. private $idCache = [];
  42. function __construct() {
  43. parent::__construct();
  44. $this->addDescription( 'Benchmark parse operation' );
  45. $this->addArg( 'title', 'The name of the page to parse' );
  46. $this->addOption( 'warmup', 'Repeat the parse operation this number of times to warm the cache',
  47. false, true );
  48. $this->addOption( 'loops', 'Number of times to repeat parse operation post-warmup',
  49. false, true );
  50. $this->addOption( 'page-time',
  51. 'Use the version of the page which was current at the given time',
  52. false, true );
  53. $this->addOption( 'tpl-time',
  54. 'Use templates which were current at the given time (except that moves and ' .
  55. 'deletes are not handled properly)',
  56. false, true );
  57. $this->addOption( 'reset-linkcache', 'Reset the LinkCache after every parse.',
  58. false, false );
  59. }
  60. function execute() {
  61. if ( $this->hasOption( 'tpl-time' ) ) {
  62. $this->templateTimestamp = wfTimestamp( TS_MW, strtotime( $this->getOption( 'tpl-time' ) ) );
  63. Hooks::register( 'BeforeParserFetchTemplateAndtitle', [ $this, 'onFetchTemplate' ] );
  64. }
  65. $this->clearLinkCache = $this->hasOption( 'reset-linkcache' );
  66. // Set as a member variable to avoid function calls when we're timing the parse
  67. $this->linkCache = MediaWikiServices::getInstance()->getLinkCache();
  68. $title = Title::newFromText( $this->getArg() );
  69. if ( !$title ) {
  70. $this->error( "Invalid title" );
  71. exit( 1 );
  72. }
  73. if ( $this->hasOption( 'page-time' ) ) {
  74. $pageTimestamp = wfTimestamp( TS_MW, strtotime( $this->getOption( 'page-time' ) ) );
  75. $id = $this->getRevIdForTime( $title, $pageTimestamp );
  76. if ( !$id ) {
  77. $this->error( "The page did not exist at that time" );
  78. exit( 1 );
  79. }
  80. $revision = Revision::newFromId( $id );
  81. } else {
  82. $revision = Revision::newFromTitle( $title );
  83. }
  84. if ( !$revision ) {
  85. $this->error( "Unable to load revision, incorrect title?" );
  86. exit( 1 );
  87. }
  88. $warmup = $this->getOption( 'warmup', 1 );
  89. for ( $i = 0; $i < $warmup; $i++ ) {
  90. $this->runParser( $revision );
  91. }
  92. $loops = $this->getOption( 'loops', 1 );
  93. if ( $loops < 1 ) {
  94. $this->error( 'Invalid number of loops specified', true );
  95. }
  96. $startUsage = getrusage();
  97. $startTime = microtime( true );
  98. for ( $i = 0; $i < $loops; $i++ ) {
  99. $this->runParser( $revision );
  100. }
  101. $endUsage = getrusage();
  102. $endTime = microtime( true );
  103. printf( "CPU time = %.3f s, wall clock time = %.3f s\n",
  104. // CPU time
  105. ( $endUsage['ru_utime.tv_sec'] + $endUsage['ru_utime.tv_usec'] * 1e-6
  106. - $startUsage['ru_utime.tv_sec'] - $startUsage['ru_utime.tv_usec'] * 1e-6 ) / $loops,
  107. // Wall clock time
  108. ( $endTime - $startTime ) / $loops
  109. );
  110. }
  111. /**
  112. * Fetch the ID of the revision of a Title that occurred
  113. *
  114. * @param Title $title
  115. * @param string $timestamp
  116. * @return bool|string Revision ID, or false if not found or error
  117. */
  118. function getRevIdForTime( Title $title, $timestamp ) {
  119. $dbr = $this->getDB( DB_REPLICA );
  120. $id = $dbr->selectField(
  121. [ 'revision', 'page' ],
  122. 'rev_id',
  123. [
  124. 'page_namespace' => $title->getNamespace(),
  125. 'page_title' => $title->getDBkey(),
  126. 'rev_timestamp <= ' . $dbr->addQuotes( $timestamp )
  127. ],
  128. __METHOD__,
  129. [ 'ORDER BY' => 'rev_timestamp DESC', 'LIMIT' => 1 ],
  130. [ 'revision' => [ 'INNER JOIN', 'rev_page=page_id' ] ]
  131. );
  132. return $id;
  133. }
  134. /**
  135. * Parse the text from a given Revision
  136. *
  137. * @param Revision $revision
  138. */
  139. function runParser( Revision $revision ) {
  140. $content = $revision->getContent();
  141. $content->getParserOutput( $revision->getTitle(), $revision->getId() );
  142. if ( $this->clearLinkCache ) {
  143. $this->linkCache->clear();
  144. }
  145. }
  146. /**
  147. * Hook into the parser's revision ID fetcher. Make sure that the parser only
  148. * uses revisions around the specified timestamp.
  149. *
  150. * @param Parser $parser
  151. * @param Title $title
  152. * @param bool &$skip
  153. * @param string|bool &$id
  154. * @return bool
  155. */
  156. function onFetchTemplate( Parser $parser, Title $title, &$skip, &$id ) {
  157. $pdbk = $title->getPrefixedDBkey();
  158. if ( !isset( $this->idCache[$pdbk] ) ) {
  159. $proposedId = $this->getRevIdForTime( $title, $this->templateTimestamp );
  160. $this->idCache[$pdbk] = $proposedId;
  161. }
  162. if ( $this->idCache[$pdbk] !== false ) {
  163. $id = $this->idCache[$pdbk];
  164. }
  165. return true;
  166. }
  167. }
  168. $maintClass = 'BenchmarkParse';
  169. require RUN_MAINTENANCE_IF_MAIN;