From: Aaron Schulz Date: Fri, 2 Sep 2016 07:30:26 +0000 (-0700) Subject: Cache revision lookups done by Parser X-Git-Tag: 1.31.0-rc.0~5770 X-Git-Url: http://git.cyclocoop.org/%7B%24admin_url%7Dmembres/cotisations/gestion/rappel_supprimer.php?a=commitdiff_plain;h=d957cb7347cc03922f116be760f14df8a630fcc9;p=lhc%2Fweb%2Fwiklou.git Cache revision lookups done by Parser Inverse flame graphs shows revision lookups as one of the big three queries (Revision, LinkCache, getTitleInfo of ResourceLoaderWikiModule). This works via a new Revision::newKnownCurrent() method needs both page/rev ID from the DB (to avoid invalidation) and fetches the user name and rev_deleted if needed (again to avoid invalidation). Parser does not care about fields anyway in the template path. Also improved cross-wiki support a bit, and fixed up some docs and IDEA errors. Change-Id: Icad602dba5de18c7758b77fd23b0a450ff21d09f --- diff --git a/includes/Revision.php b/includes/Revision.php index 2580668621..36e27bd339 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -25,52 +25,60 @@ use MediaWiki\Linker\LinkTarget; * @todo document */ class Revision implements IDBAccessObject { + /** @var int|null */ protected $mId; - - /** - * @var int|null - */ + /** @var int|null */ protected $mPage; + /** @var string */ protected $mUserText; + /** @var string */ protected $mOrigUserText; + /** @var int */ protected $mUser; + /** @var bool */ protected $mMinorEdit; + /** @var string */ protected $mTimestamp; + /** @var int */ protected $mDeleted; + /** @var int */ protected $mSize; + /** @var string */ protected $mSha1; + /** @var int */ protected $mParentId; + /** @var string */ protected $mComment; + /** @var string */ protected $mText; + /** @var int */ protected $mTextId; + /** @var int */ + protected $mUnpatrolled; - /** - * @var stdClass|null - */ + /** @var stdClass|null */ protected $mTextRow; - /** - * @var null|Title - */ + /** @var null|Title */ protected $mTitle; + /** @var bool */ protected $mCurrent; + /** @var string */ protected $mContentModel; + /** @var string */ protected $mContentFormat; - /** - * @var Content|null|bool - */ + /** @var Content|null|bool */ protected $mContent; - - /** - * @var null|ContentHandler - */ + /** @var null|ContentHandler */ protected $mContentHandler; - /** - * @var int - */ + /** @var int */ protected $mQueryFlags = 0; + /** @var bool Used for cached values to reload user text and rev_deleted */ + protected $mRefreshMutableFields = false; + /** @var string Wiki ID; false means the current wiki */ + protected $mWiki = false; // Revision deletion constants const DELETED_TEXT = 1; @@ -341,8 +349,14 @@ class Revision implements IDBAccessObject { */ private static function loadFromConds( $db, $conditions, $flags = 0 ) { $row = self::fetchFromConds( $db, $conditions, $flags ); + if ( $row ) { + $rev = new Revision( $row ); + $rev->mWiki = $db->getWikiID(); - return $row ? new Revision( $row ) : null; + return $rev; + } + + return null; } /** @@ -789,20 +803,24 @@ class Revision implements IDBAccessObject { } // rev_id is defined as NOT NULL, but this revision may not yet have been inserted. if ( $this->mId !== null ) { - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_SLAVE, [], $this->mWiki ); $row = $dbr->selectRow( [ 'page', 'revision' ], self::selectPageFields(), - [ 'page_id=rev_page', - 'rev_id' => $this->mId ], - __METHOD__ ); + [ 'page_id=rev_page', 'rev_id' => $this->mId ], + __METHOD__ + ); if ( $row ) { + // @TODO: better foreign title handling $this->mTitle = Title::newFromRow( $row ); } } - if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) { - $this->mTitle = Title::newFromID( $this->mPage ); + if ( $this->mWiki === false || $this->mWiki === wfWikiID() ) { + // Loading by ID is best, though not possible for foreign titles + if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) { + $this->mTitle = Title::newFromID( $this->mPage ); + } } return $this->mTitle; @@ -874,6 +892,8 @@ class Revision implements IDBAccessObject { * @return string */ public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) { + $this->loadMutableFields(); + if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) { return ''; } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) { @@ -990,7 +1010,14 @@ class Revision implements IDBAccessObject { * @return bool */ public function isDeleted( $field ) { - return ( $this->mDeleted & $field ) == $field; + if ( $this->isCurrent() && $field === self::DELETED_TEXT ) { + // Current revisions of pages cannot have the content hidden. Skipping this + // check is very useful for Parser as it fetches templates using newKnownCurrent(). + // Calling getVisibility() in that case triggers a verification database query. + return false; // no need to check + } + + return ( $this->getVisibility() & $field ) == $field; } /** @@ -999,6 +1026,8 @@ class Revision implements IDBAccessObject { * @return int */ public function getVisibility() { + $this->loadMutableFields(); + return (int)$this->mDeleted; } @@ -1702,7 +1731,7 @@ class Revision implements IDBAccessObject { * @return bool */ public function userCan( $field, User $user = null ) { - return self::userCanBitfield( $this->mDeleted, $field, $user ); + return self::userCanBitfield( $this->getVisibility(), $field, $user ); } /** @@ -1846,4 +1875,60 @@ class Revision implements IDBAccessObject { } return true; } + + /** + * Load a revision based on a known page ID and current revision ID from the DB + * + * This method allows for the use of caching, though accessing anything that normally + * requires permission checks (aside from the text) will trigger a small DB lookup. + * The title will also be lazy loaded, though setTitle() can be used to preload it. + * + * @param IDatabase $db + * @param int $pageId Page ID + * @param int $revId Known current revision of this page + * @return Revision|bool Returns false if missing + * @since 1.28 + */ + public static function newKnownCurrent( IDatabase $db, $pageId, $revId ) { + $cache = ObjectCache::getMainWANInstance(); + return $cache->getWithSetCallback( + // Page/rev IDs passed in from DB to reflect history merges + $cache->makeGlobalKey( 'revision', $db->getWikiID(), $pageId, $revId ), + $cache::TTL_WEEK, + function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) { + $setOpts += Database::getCacheSetOptions( $db ); + + $rev = Revision::loadFromPageId( $db, $pageId, $revId ); + // Reflect revision deletion and user renames + if ( $rev ) { + $rev->mTitle = null; // mutable; lazy-load + $rev->mRefreshMutableFields = true; + } + + return $rev ?: false; // don't cache negatives + } + ); + } + + /** + * For cached revisions, make sure the user name and rev_deleted is up-to-date + */ + private function loadMutableFields() { + if ( !$this->mRefreshMutableFields ) { + return; // not needed + } + + $this->mRefreshMutableFields = false; + $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_SLAVE, [], $this->mWiki ); + $row = $dbr->selectRow( + [ 'revision', 'user' ], + [ 'rev_deleted', 'user_name' ], + [ 'rev_id' => $this->mId, 'user_id = rev_user' ], + __METHOD__ + ); + if ( $row ) { // update values + $this->mDeleted = (int)$row->rev_deleted; + $this->mUserText = $row->user_name; + } + } } diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index b4d9c7002b..a93400be02 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -3458,10 +3458,18 @@ class Parser { * @since 1.24 * @param Title $title * @param Parser|bool $parser - * @return Revision + * @return Revision|bool False if missing */ - public static function statelessFetchRevision( $title, $parser = false ) { - return Revision::newFromTitle( $title ); + public static function statelessFetchRevision( Title $title, $parser = false ) { + $pageId = $title->getArticleID(); + $revId = $title->getLatestRevID(); + + $rev = Revision::newKnownCurrent( wfGetDB( DB_SLAVE ), $pageId, $revId ); + if ( $rev ) { + $rev->setTitle( $title ); + } + + return $rev; } /**