Bartosz Dziewoński has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/297347

Change subject: mw.FormDataTransport: Work around call stack limits for chunked 
uploads
......................................................................

mw.FormDataTransport: Work around call stack limits for chunked uploads

In jQuery 2.x, nested promises result in nested call stacks when
resolving/rejecting/notifying the last promise in the chain and
listening on the first one, and browsers have call stack limits low
enough that we previously ran into them for files around a couple
hundred megabytes (the worst is Firefox 47 with a limit of 1024
calls).

Write some custom logic without chaining (just manual calls to
done/fail/progress and resolve/reject/notify), so that we never have
to go through 800 function calls when finishing the upload and
resolving a promise.

Bug: T130610
Change-Id: Ia34134f4aac58d80a2752efe8103fdf3646e6078
---
M resources/transports/mw.FormDataTransport.js
1 file changed, 51 insertions(+), 16 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/UploadWizard 
refs/changes/47/297347/1

diff --git a/resources/transports/mw.FormDataTransport.js 
b/resources/transports/mw.FormDataTransport.js
index d97fb12..e637d17 100644
--- a/resources/transports/mw.FormDataTransport.js
+++ b/resources/transports/mw.FormDataTransport.js
@@ -107,8 +107,7 @@
         * @return {jQuery.Promise}
         */
        mw.FormDataTransport.prototype.upload = function ( file ) {
-               var formData, deferred, ext, totalUploaded,
-                       chunkSize = this.chunkSize,
+               var formData, deferred, ext,
                        transport = this;
 
                // use timestamp + filename to avoid conflicts on server
@@ -123,18 +122,8 @@
                        this.tempname = this.tempname.substr( 0, 240 - 
ext.length - 1 ) + '.' + ext;
                }
 
-               if ( file.size > chunkSize ) {
-                       totalUploaded = 0;
-                       // The progress notifications give us per-chunk 
progress, filter them to get progress
-                       // for the whole file
-                       return this.uploadChunk( file, 0 ).then( null, null, 
function ( fraction ) {
-                               if ( fraction === 1 ) {
-                                       // We completed a chunk
-                                       totalUploaded += chunkSize;
-                                       fraction = 0;
-                               }
-                               return ( totalUploaded + fraction * chunkSize ) 
/ file.size;
-                       } );
+               if ( file.size > this.chunkSize ) {
+                       return this.chunkedUpload( file );
                } else {
                        deferred = $.Deferred();
                        this.xhr = this.createXHR( deferred );
@@ -155,6 +144,53 @@
 
                        return deferred.promise();
                }
+       };
+
+       /**
+        * This function exists to safely chain several hundred promises 
without using .then() or nested
+        * promises. We might divide a 4 GB file into 800 chunks of 5 MB each.
+        *
+        * In jQuery 2.x, nested promises result in nested call stacks when 
resolving/rejecting/notifying
+        * the last promise in the chain and listening on the first one, and 
browsers have call stack
+        * limits low enough that we previously ran into them for files around 
a couple hundred megabytes
+        * (the worst is Firefox 47 with a limit of 1024 calls).
+        *
+        * @param {File} file
+        * @return {jQuery.Promise} Promise which behaves identically to a 
regular non-chunked upload
+        *   promise from #upload
+        */
+       mw.FormDataTransport.prototype.chunkedUpload = function ( file ) {
+               var
+                       offset,
+                       prevPromise = $.Deferred().resolve(),
+                       deferred = $.Deferred(),
+                       fileSize = file.size,
+                       chunkSize = this.chunkSize,
+                       transport = this;
+
+               for ( offset = 0; offset < fileSize; offset += chunkSize ) {
+                       /*jshint loopfunc:true */
+                       // Capture offset in a closure
+                       ( function ( offset ) {
+                               var
+                                       newPromise = $.Deferred(),
+                                       isLastChunk = offset + chunkSize >= 
fileSize,
+                                       thisChunkSize = isLastChunk ? ( 
fileSize % chunkSize ) : chunkSize;
+                               prevPromise.done( function () {
+                                       transport.uploadChunk( file, offset )
+                                               .done( isLastChunk ? 
deferred.resolve : newPromise.resolve )
+                                               .fail( deferred.reject )
+                                               .progress( function ( fraction 
) {
+                                                       // The progress 
notifications give us per-chunk progress.
+                                                       // Calculate progress 
for the whole file.
+                                                       deferred.notify( ( 
offset + fraction * thisChunkSize ) / fileSize );
+                                               } );
+                               } );
+                               prevPromise = newPromise;
+                       } )( offset );
+               }
+
+               return deferred.promise();
        };
 
        /**
@@ -221,8 +257,7 @@
                                        case 'Continue':
                                                // Reset retry counter
                                                transport.retries = 0;
-                                               // Start uploading next chunk
-                                               return transport.uploadChunk( 
file, response.upload.offset );
+                                               /* falls through */
                                        case 'Success':
                                                // Just pass the response 
through.
                                                return response;

-- 
To view, visit https://gerrit.wikimedia.org/r/297347
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ia34134f4aac58d80a2752efe8103fdf3646e6078
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/UploadWizard
Gerrit-Branch: master
Gerrit-Owner: Bartosz Dziewoński <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to