Use serialization cache in MW integration

Add prepareCacheKey() which submits HTML for serialization and saves
the resulting cache key, and tryWithPreparedCacheKey() which uses that
cache key (if available; if pending, it waits for it) for API requests.
Implemented save(), serialize() and showChanges() in terms of
tryWithPreparedCacheKey().

When opening the save dialog, run the conversion, cache it, and fire
off a prepareCacheKey(). Then use the cached conversion for save/diff/
serialize. This means we don't convert multiple times, and it causes
the prepared wikitext to be used.

Bug: 55979
Bug: 56011
Change-Id: I1d56fe88d312e9810a57d56a285ccdf4f1facf42
This commit is contained in:
Roan Kattouw 2013-11-06 00:22:11 -08:00
parent 10702d9fa0
commit 1919fffc74
2 changed files with 218 additions and 103 deletions

View file

@ -755,7 +755,6 @@ ve.init.mw.ViewPageTarget.prototype.updateToolbarSaveButtonState = function () {
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onSaveDialogReview = function () {
var doc = this.surface.getModel().getDocument();
this.sanityCheckVerified = true;
this.saveDialog.setSanityCheck( this.sanityCheckVerified );
@ -769,14 +768,9 @@ ve.init.mw.ViewPageTarget.prototype.onSaveDialogReview = function () {
this.saveDialog.$loadingIcon.show();
if ( this.pageExists ) {
// Has no callback, handled via target.onShowChanges
this.showChanges(
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList(), doc.getInnerWhitespace() )
);
this.showChanges( this.docToSave );
} else {
this.serialize(
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList(), doc.getInnerWhitespace() ),
ve.bind( this.onSerialize, this )
);
this.serialize( this.docToSave, ve.bind( this.onSerialize, this ) );
}
} else {
this.saveDialog.swapPanel( 'review' );
@ -801,8 +795,7 @@ ve.init.mw.ViewPageTarget.prototype.onSaveDialogSave = function () {
* Try to save the current document.
*/
ve.init.mw.ViewPageTarget.prototype.saveDocument = function () {
var doc = this.surface.getModel().getDocument(),
saveOptions = this.getSaveOptions();
var saveOptions = this.getSaveOptions();
// Reset any old captcha data
if ( this.captcha ) {
@ -824,10 +817,7 @@ ve.init.mw.ViewPageTarget.prototype.saveDocument = function () {
} else {
this.saveDialog.saveButton.setDisabled( true );
this.saveDialog.$loadingIcon.show();
this.save(
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList(), doc.getInnerWhitespace() ),
saveOptions
);
this.save( this.docToSave, saveOptions );
}
};
@ -847,7 +837,7 @@ ve.init.mw.ViewPageTarget.prototype.editSource = function () {
}
// Get Wikitext from the DOM
this.serialize(
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ),
this.docToSave || ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ),
ve.bind( function ( wikitext ) {
var $form,
options = this.getSaveOptions(),
@ -880,11 +870,10 @@ ve.init.mw.ViewPageTarget.prototype.editSource = function () {
*
* @method
*/
ve.init.mw.ViewPageTarget.prototype.onSaveDialogResolveConflict= function () {
var doc = this.surface.getModel().getDocument();
ve.init.mw.ViewPageTarget.prototype.onSaveDialogResolveConflict = function () {
// Get Wikitext from the DOM, and set up a submit call when it's done
this.serialize(
ve.dm.converter.getDomFromData( doc.getFullData(), doc.getStore(), doc.getInternalList() ),
this.docToSave,
ve.bind( function ( wikitext ) {
this.submit( wikitext, this.getSaveOptions() );
}, this )
@ -1247,6 +1236,15 @@ ve.init.mw.ViewPageTarget.prototype.setupSaveDialog = function () {
* @method
*/
ve.init.mw.ViewPageTarget.prototype.showSaveDialog = function () {
// Preload the serialization
var doc = this.surface.getModel().getDocument();
if ( !this.docToSave ) {
this.docToSave = ve.dm.converter.getDomFromData(
doc.getFullData(), doc.getStore(), doc.getInternalList(), doc.getInnerWhitespace()
);
}
this.prepareCacheKey( this.docToSave );
this.saveDialog.setSanityCheck( this.sanityCheckVerified );
this.surface.getDialogs().getWindow( 'mwSave' ).open();
this.timings.saveDialogOpen = ve.now();
@ -1259,6 +1257,17 @@ ve.init.mw.ViewPageTarget.prototype.showSaveDialog = function () {
* Respond to the save dialog being closed.
*/
ve.init.mw.ViewPageTarget.prototype.onSaveDialogClose = function () {
// Clear the cached HTML and cache key once the document changes
var clear = ve.bind( function () {
this.docToSave = null;
this.clearPreparedCacheKey();
}, this );
if ( this.surface ) {
this.surface.getModel().getDocument().once( 'transact', clear );
} else {
clear();
}
ve.track( 'behavior.saveDialogClose', { 'duration': ve.now() - this.timings.saveDialogOpen } );
this.timings.saveDialogOpen = null;
};

View file

@ -58,8 +58,10 @@ ve.init.mw.Target = function VeInitMwTarget( $container, pageName, revisionId )
this.pluginCallbacks = [];
this.modulesReady = $.Deferred();
this.preparedCacheKeyPromise = null;
this.loading = false;
this.saving = false;
this.diffing = false;
this.serializing = false;
this.submitting = false;
this.baseTimeStamp = null;
@ -346,6 +348,7 @@ ve.init.mw.Target.onSaveError = function ( jqXHR, status, data ) {
*/
ve.init.mw.Target.onShowChanges = function ( response ) {
var data = response.visualeditor;
this.diffing = false;
if ( !data && !response.error ) {
ve.init.mw.Target.onShowChangesError.call( this, null, 'Invalid response from server', null );
} else if ( response.error ) {
@ -366,7 +369,7 @@ ve.init.mw.Target.onShowChanges = function ( response ) {
};
/**
* Handle errors during saveChanges action.
* Handle errors during showChanges action.
*
* @static
* @method
@ -377,7 +380,7 @@ ve.init.mw.Target.onShowChanges = function ( response ) {
* @fires showChangesError
*/
ve.init.mw.Target.onShowChangesError = function ( jqXHR, status, error ) {
this.saving = false;
this.diffing = false;
this.emit( 'showChangesError', jqXHR, status, error );
};
@ -530,14 +533,14 @@ ve.init.mw.Target.prototype.load = function () {
start = ve.now();
this.loading = $.ajax( {
'url': this.apiUrl,
'data': data,
'dataType': 'json',
'type': 'POST',
// Wait up to 100 seconds before giving up
'timeout': 100000,
'cache': 'false'
} )
'url': this.apiUrl,
'data': data,
'dataType': 'json',
'type': 'POST',
// Wait up to 100 seconds before giving up
'timeout': 100000,
'cache': 'false'
} )
.then( function ( data, status, jqxhr ) {
ve.track( 'performance.system.domLoad', {
'bytes': $.byteLength( jqxhr.responseText ),
@ -553,6 +556,162 @@ ve.init.mw.Target.prototype.load = function () {
return true;
};
/**
* Serialize the current document and store the result in the serialization cache on the server.
*
* This function returns a promise that is resolved once serialization is complete, with the
* cache key passed as the first parameter.
*
* If there's already a request pending for the same (reference-identical) HTMLDocument, this
* function will not initiate a new request but will return the promise for the pending request.
* If a request for the same document has already been completed, this function will keep returning
* the same promise (which will already have been resolved) until clearPreparedCacheKey() is called.
*
* @param {HTMLDocument} doc Document to serialize
* @returns {jQuery.Promise} Abortable promise, resolved with the cache key.
*/
ve.init.mw.Target.prototype.prepareCacheKey = function ( doc ) {
var xhr, html, start = ve.now(), deferred = $.Deferred();
if ( this.preparedCacheKeyPromise && this.preparedCacheKeyPromise.doc === doc ) {
return this.preparedCacheKeyPromise;
}
this.clearPreparedCacheKey();
html = this.getHtml( doc );
xhr = $.ajax( {
'url': this.apiUrl,
'data': {
'action': 'visualeditor',
'paction': 'serializeforcache',
'html': html,
'page': this.pageName,
'oldid': this.revid,
'format': 'json'
},
'dataType': 'json',
'type': 'POST',
// Wait up to 100 seconds before giving up
'timeout': 100000,
'cache': 'false'
} )
.done( function ( response ) {
var trackData = { 'duration': ve.now() - start };
if ( response.visualeditor && typeof response.visualeditor.cachekey === 'string' ) {
ve.track( 'performance.system.serializeforcache', trackData );
deferred.resolve( response.visualeditor.cachekey );
} else {
ve.track( 'performance.system.serializeforcache.nocachekey', trackData );
deferred.reject();
}
} )
.fail( function () {
ve.track( 'performance.system.serializeforcache.fail', { 'duration': ve.now() - start } );
deferred.reject();
} );
this.preparedCacheKeyPromise = deferred.promise( {
'abort': xhr.abort,
'html': html,
'doc': doc
} );
return this.preparedCacheKeyPromise;
};
/**
* Get the prepared wikitext, if any. Same as prepareWikitext() but does not initiate a request
* if one isn't already pending or finished. Instead, it returns a rejected promise in that case.
*
* @param {HTMLDocument} doc Document to serialize
* @returns {jQuery.Promise} Abortable promise, resolved with the cache key.
*/
ve.init.mw.Target.prototype.getPreparedCacheKey = function ( doc ) {
var deferred;
if ( this.preparedCacheKeyPromise && this.preparedCacheKeyPromise.doc === doc ) {
return this.preparedCacheKeyPromise;
}
deferred = $.Deferred();
deferred.reject();
return deferred.promise();
};
/**
* Clear the promise for the prepared wikitext cache key, and abort it if it's still in progress.
*/
ve.init.mw.Target.prototype.clearPreparedCacheKey = function () {
if ( this.preparedCacheKeyPromise ) {
this.preparedCacheKeyPromise.abort();
this.preparedCacheKeyPromise = null;
}
};
/**
* Try submitting an API request with a cache key for prepared wikitext, falling back to submitting
* HTML directly if there is no cache key present or pending, or if the request for the cache key
* fails, or if using the cache key fails with a badcachekey error.
*
* @param {HTMLDocument} doc Document to submit
* @param {Object} options POST parameters to send. Do not include 'html', 'cachekey' or 'format'.
* @param {string} [eventName] If set, log an event when the request completes successfully. The
* full event name used will be 'performance.system.{eventName}.withCacheKey' or .withoutCacheKey
* depending on whether or not a cache key was used.
* @returns {jQuery.Promise}
*/
ve.init.mw.Target.prototype.tryWithPreparedCacheKey = function ( doc, options, eventName ) {
var data, preparedCacheKey = this.getPreparedCacheKey( doc ), target = this;
data = $.extend( {}, options, { 'format': 'json' } );
function ajaxRequest( cachekey ) {
var start = ve.now();
if ( typeof cachekey === 'string' ) {
data.cachekey = cachekey;
} else {
// Getting a cache key failed, fall back to sending the HTML
data.html = preparedCacheKey && preparedCacheKey.html || target.getHtml( doc );
// If using the cache key fails, we'll come back here with cachekey still set
delete data.cachekey;
}
return $.ajax( {
'url': target.apiUrl,
'data': data,
'dataType': 'json',
'type': 'POST',
// Wait up to 100 seconds before giving up
'timeout': 100000
} )
.then( function ( response, status, jqxhr ) {
var fullEventName, eventData = {
'bytes': $.byteLength( jqxhr.responseText ),
'duration': ve.now() - start,
'parsoid': jqxhr.getResponseHeader( 'X-Parsoid-Performance' )
};
if ( response.error && response.error.code === 'badcachekey' ) {
// Log the failure if eventName was set
if ( eventName ) {
fullEventName = 'performance.system.' + eventName + '.badCacheKey';
ve.track( fullEventName, eventData );
}
// This cache key is evidently bad, clear it
target.clearPreparedCacheKey();
// Try again without a cache key
return ajaxRequest( null );
}
// Log data about the request if eventName was set
if ( eventName ) {
fullEventName = 'performance.system.' + eventName +
( typeof cachekey === 'string' ? '.withCacheKey' : '.withoutCacheKey' );
ve.track( fullEventName, eventData );
}
return jqxhr;
} );
}
// If we successfully get prepared wikitext, then invoke ajaxRequest() with the cache key,
// otherwise invoke it without.
return preparedCacheKey.then( ajaxRequest, ajaxRequest );
};
/**
* Post DOM data to the Parsoid API.
*
@ -569,42 +728,22 @@ ve.init.mw.Target.prototype.load = function () {
* @returns {boolean} Saving has been started
*/
ve.init.mw.Target.prototype.save = function ( doc, options ) {
var data, start;
var data;
// Prevent duplicate requests
if ( this.saving ) {
return false;
}
data = $.extend( {}, options, {
'format': 'json',
'action': 'visualeditoredit',
'page': this.pageName,
'oldid': this.revid,
'basetimestamp': this.baseTimeStamp,
'starttimestamp': this.startTimeStamp,
'html': this.getHtml( doc ),
'token': this.editToken
} );
// Save DOM
start = ve.now();
this.saving = $.ajax( {
'url': this.apiUrl,
'data': data,
'dataType': 'json',
'type': 'POST',
// Wait up to 100 seconds before giving up
'timeout': 100000
} )
.then( function ( data, status, jqxhr ) {
ve.track( 'performance.system.domSave', {
'bytes': $.byteLength( jqxhr.responseText ),
'duration': ve.now() - start,
'parsoid': jqxhr.getResponseHeader( 'X-Parsoid-Performance' )
} );
return jqxhr;
} )
this.saving = this.tryWithPreparedCacheKey( doc, data, 'save' )
.done( ve.bind( ve.init.mw.Target.onSave, this ) )
.fail( ve.bind( ve.init.mw.Target.onSaveError, this ) );
@ -612,38 +751,26 @@ ve.init.mw.Target.prototype.save = function ( doc, options ) {
};
/**
* Post DOM data to the Parsoid API to retreive wikitext diff.
* Post DOM data to the Parsoid API to retrieve wikitext diff.
*
* @method
* @param {HTMLDocument} doc Document to compare against (via wikitext)
* @returns {boolean} Diffing has been started
*/
ve.init.mw.Target.prototype.showChanges = function ( doc ) {
var start = ve.now();
$.ajax( {
'url': this.apiUrl,
'data': {
'format': 'json',
'action': 'visualeditor',
'paction': 'diff',
'page': this.pageName,
'oldid': this.revid,
'html': this.getHtml( doc )
},
'dataType': 'json',
'type': 'POST',
// Wait up to 100 seconds before giving up
'timeout': 100000
} )
.then( function ( data, status, jqxhr ) {
ve.track( 'performance.system.domDiff', {
'bytes': $.byteLength( jqxhr.responseText ),
'duration': ve.now() - start,
'parsoid': jqxhr.getResponseHeader( 'X-Parsoid-Performance' )
} );
return jqxhr;
} )
if ( this.diffing ) {
return false;
}
this.diffing = this.tryWithPreparedCacheKey( doc, {
'action': 'visualeditor',
'paction': 'diff',
'page': this.pageName,
'oldid': this.revid,
}, 'diff' )
.done( ve.bind( ve.init.mw.Target.onShowChanges, this ) )
.fail( ve.bind( ve.init.mw.Target.onShowChangesError, this ) );
return true;
};
/**
@ -706,41 +833,20 @@ ve.init.mw.Target.prototype.submit = function ( wikitext, options ) {
* @method
* @param {HTMLDocument} doc Document to serialize
* @param {Function} callback Function to call when complete, accepts error and wikitext arguments
* @returns {boolean} Serializing has beeen started
* @returns {boolean} Serializing has been started
*/
ve.init.mw.Target.prototype.serialize = function ( doc, callback ) {
var start = ve.now();
// Prevent duplicate requests
if ( this.serializing ) {
return false;
}
// Load DOM
this.serializing = true;
this.serializeCallback = callback;
$.ajax( {
'url': this.apiUrl,
'data': {
'action': 'visualeditor',
'paction': 'serialize',
'html': this.getHtml( doc ),
'page': this.pageName,
'oldid': this.revid,
'format': 'json'
},
'dataType': 'json',
'type': 'POST',
// Wait up to 100 seconds before giving up
'timeout': 100000,
'cache': 'false'
} )
.then( function ( data, status, jqxhr ) {
ve.track( 'performance.system.domSerialize', {
'bytes': $.byteLength( jqxhr.responseText ),
'duration': ve.now() - start,
'parsoid': jqxhr.getResponseHeader( 'X-Parsoid-Performance' )
} );
return jqxhr;
} )
this.serializing = this.tryWithPreparedCacheKey( doc, {
'action': 'visualeditor',
'paction': 'serialize',
'page': this.pageName,
'oldid': this.revid
}, 'serialize' )
.done( ve.bind( ve.init.mw.Target.onSerialize, this ) )
.fail( ve.bind( ve.init.mw.Target.onSerializeError, this ) );
return true;