From 17431af1544ca4d5869656296576b3018b57d8f6 Mon Sep 17 00:00:00 2001 From: Aaron Schulz Date: Thu, 4 Dec 2014 01:42:20 -0800 Subject: [PATCH] Reuse page preview parses by using the edit stash system * This also changes previews to render section edit tokens but remove them on output, avoiding cache fragmentation. * Also shortened the resulting getStashKey() value. Change-Id: Ic8fa87669106b960c76912b864788b781f6ee2e6 --- includes/EditPage.php | 15 ++-- includes/api/ApiStashEdit.php | 143 ++++++++++++++++++++++++------ includes/parser/ParserOptions.php | 27 ++++++ 3 files changed, 153 insertions(+), 32 deletions(-) diff --git a/includes/EditPage.php b/includes/EditPage.php index db2e442bf6..993c20acc9 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -3496,7 +3496,6 @@ HTML } $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() ); - $parserOptions->setEditSection( false ); $parserOptions->setIsPreview( true ); $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' ); @@ -3548,13 +3547,17 @@ HTML # For CSS/JS pages, we should have called the ShowRawCssJs hook here. # But it's now deprecated, so never mind - $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions ); - $parserOutput = $content->getParserOutput( - $this->getArticle()->getTitle(), - null, - $parserOptions + $pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions ); + $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions ); + + # Try to stash the edit for the final submission step + # @todo: different date format preferences cause cache misses + ApiStashEdit::stashEditFromPreview( + $this->getArticle(), $content, $pstContent, + $parserOutput, $parserOptions, $parserOptions, wfTimestampNow() ); + $parserOutput->setEditSectionTokens( false ); // no section edit links $previewHTML = $parserOutput->getText(); $this->mParserOutput = $parserOutput; $wgOut->addParserOutputMetadata( $parserOutput ); diff --git a/includes/api/ApiStashEdit.php b/includes/api/ApiStashEdit.php index 2bb0b5c1ca..b32d332966 100644 --- a/includes/api/ApiStashEdit.php +++ b/includes/api/ApiStashEdit.php @@ -121,17 +121,10 @@ class ApiStashEdit extends ApiBase { } if ( $editInfo && $editInfo->output ) { - $parserOutput = $editInfo->output; - // If an item is renewed, mind the cache TTL determined by config and parser functions - $since = time() - wfTimestamp( TS_UNIX, $parserOutput->getTimestamp() ); - $ttl = min( $parserOutput->getCacheExpiry() - $since, 5 * 60 ); - if ( $ttl > 0 && !$parserOutput->getFlag( 'vary-revision' ) ) { - // Only store what is actually needed - $stashInfo = (object)array( - 'pstContent' => $editInfo->pstContent, - 'output' => $editInfo->output, - 'timestamp' => $editInfo->timestamp - ); + list( $stashInfo, $ttl ) = self::buildStashValue( + $editInfo->pstContent, $editInfo->output, $editInfo->timestamp + ); + if ( $stashInfo ) { $ok = $wgMemc->set( $key, $stashInfo, $ttl ); if ( $ok ) { $status = 'stashed'; @@ -150,24 +143,69 @@ class ApiStashEdit extends ApiBase { } /** - * Get the temporary prepared edit stash key for a user + * Attempt to cache PST content and corresponding parser output in passing * - * @param Title $title - * @param Content $content - * @param User $user User to get parser options from - * @return string + * This method can be called when the output was already generated for other + * reasons. Parsing should not be done just to call this method, however. + * $pstOpts must be that of the user doing the edit preview. If $pOpts does + * not match the options of WikiPage::makeParserOptions( 'canonical' ), this + * will do nothing. Provided the values are cacheable, they will be stored + * in memcached so that final edit submission might make use of them. + * + * @param Article|WikiPage $page Page title + * @param Content $content Proposed page content + * @param Content $pstContent The result of preSaveTransform() on $content + * @param ParserOutput $pOut The result of getParserOutput() on $pstContent + * @param ParserOptions $pstOpts Options for $pstContent (MUST be for prospective author) + * @param ParserOptions $pOpts Options for $pOut + * @param string $timestamp TS_MW timestamp of parser output generation + * @return boolean Success */ - protected static function getStashKey( - Title $title, Content $content, User $user + public static function stashEditFromPreview( + Page $page, Content $content, Content $pstContent, ParserOutput $pOut, + ParserOptions $pstOpts, ParserOptions $pOpts, $timestamp ) { - return wfMemcKey( 'prepared-edit', - md5( $title->getPrefixedDBkey() ), // handle rename races - $content->getModel(), - $content->getDefaultFormat(), - sha1( $content->serialize( $content->getDefaultFormat() ) ), - $user->getId() ?: md5( $user->getName() ), // account for user parser options - $user->getId() ? $user->getTouched() : '-' // handle preference change races - ); + global $wgMemc; + + // getIsPreview() controls parser function behavior that references things + // like user/revision that don't exists yet. The user/text should already + // be set correctly by callers, just double check the preview flag. + if ( !$pOpts->getIsPreview() ) { + return false; // sanity + } elseif ( $pOpts->getIsSectionPreview() ) { + return false; // short-circuit (need the full content) + } + + // PST parser options are for the user (handles signatures, etc...) + $user = $pstOpts->getUser(); + // Get a key based on the source text, format, and user preferences + $key = self::getStashKey( $page->getTitle(), $content, $user ); + + // Parser output options must match cannonical options. + // Treat some options as matching that are different but don't matter. + $canonicalPOpts = $page->makeParserOptions( 'canonical' ); + $canonicalPOpts->setIsPreview( true ); // force match + $canonicalPOpts->setTimestamp( $pOpts->getTimestamp() ); // force match + if ( !$pOpts->matches( $canonicalPOpts ) ) { + wfDebugLog( 'StashEdit', "Uncacheable preview output for key '$key' (options)." ); + return false; + } + + // Build a value to cache with a proper TTL + list( $stashInfo, $ttl ) = self::buildStashValue( $pstContent, $pOut, $timestamp ); + if ( !$stashInfo ) { + wfDebugLog( 'StashEdit', "Uncacheable parser output for key '$key' (rev/TTL)." ); + return false; + } + + $ok = $wgMemc->set( $key, $stashInfo, $ttl ); + if ( !$ok ) { + wfDebugLog( 'StashEdit', "Failed to cache preview parser output for key '$key'." ); + } else { + wfDebugLog( 'StashEdit', "Cached preview output for key '$key'." ); + } + + return $ok; } /** @@ -253,6 +291,59 @@ class ApiStashEdit extends ApiBase { return $editInfo; } + /** + * Get the temporary prepared edit stash key for a user + * + * This key can be used for caching prepared edits provided: + * - a) The $user was used for PST options + * - b) The parser output was made from the PST using cannonical matching options + * + * @param Title $title + * @param Content $content + * @param User $user User to get parser options from + * @return string + */ + protected static function getStashKey( Title $title, Content $content, User $user ) { + $hash = sha1( implode( ':', array( + $content->getModel(), + $content->getDefaultFormat(), + sha1( $content->serialize( $content->getDefaultFormat() ) ), + $user->getId() ?: md5( $user->getName() ), // account for user parser options + $user->getId() ? $user->getTouched() : '-' // handle preference change races + ) ) ); + + return wfMemcKey( 'prepared-edit', md5( $title->getPrefixedDBkey() ), $hash ); + } + + /** + * Build a value to store in memcached based on the PST content and parser output + * + * This makes a simple version of WikiPage::prepareContentForEdit() as stash info + * + * @param Content $pstContent + * @param ParserOutput $parserOutput + * @param string $timestamp TS_MW + * @return array (stash info array, TTL in seconds) or (null, 0) + */ + protected static function buildStashValue( + Content $pstContent, ParserOutput $parserOutput, $timestamp + ) { + // If an item is renewed, mind the cache TTL determined by config and parser functions + $since = time() - wfTimestamp( TS_UNIX, $parserOutput->getTimestamp() ); + $ttl = min( $parserOutput->getCacheExpiry() - $since, 5 * 60 ); + if ( $ttl > 0 && !$parserOutput->getFlag( 'vary-revision' ) ) { + // Only store what is actually needed + $stashInfo = (object)array( + 'pstContent' => $pstContent, + 'output' => $parserOutput, + 'timestamp' => $timestamp + ); + return array( $stashInfo, $ttl ); + } + + return array( null, 0 ); + } + public function getAllowedParams() { return array( 'title' => array( diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php index b570fa5eaa..a61dbf0d3b 100644 --- a/includes/parser/ParserOptions.php +++ b/includes/parser/ParserOptions.php @@ -641,6 +641,7 @@ class ParserOptions { wfProfileIn( __METHOD__ ); + // *UPDATE* ParserOptions::matches() if any of this changes as needed $this->mInterwikiMagic = $wgInterwikiMagic; $this->mAllowExternalImages = $wgAllowExternalImages; $this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom; @@ -666,6 +667,32 @@ class ParserOptions { wfProfileOut( __METHOD__ ); } + /** + * Check if these options match that of another options set + * + * This ignores report limit settings that only affect HTML comments + * + * @return bool + * @since 1.25 + */ + public function matches( ParserOptions $other ) { + $fields = array_keys( get_class_vars( __CLASS__ ) ); + $fields = array_diff( $fields, array( + 'mEnableLimitReport', // only effects HTML comments + 'onAccessCallback', // only used for ParserOutput option tracking + ) ); + foreach ( $fields as $field ) { + if ( !is_object( $this->$field ) && $this->$field !== $other->$field ) { + return false; + } + } + // Check the object and lazy-loaded options + return ( + $this->mUserLang->getCode() === $other->mUserLang->getCode() && + $this->getDateFormat() === $other->getDateFormat() + ); + } + /** * Registers a callback for tracking which ParserOptions which are used. * This is a private API with the parser. -- 2.20.1