Problema de memory de Chrome – File API + AngularJS

Tengo una aplicación web que necesita cargar files grandes al almacenamiento de Azure BLOB. Mi solución utiliza HTML5 File API para dividir en segmentos que luego se colocan como blob blocks, los ID de los bloques se almacenan en una matriz y luego los bloques se confirman como blob.

La solución funciona bien en IE. En Chrome de 64 bits, he cargado correctamente files de 4 Gb pero veo un uso de memory muy intenso (2 Gb +). En Chrome de 32 bits, el process de Chrome específico llegará a alnetworkingedor de 500-550Mb y luego se bloqueará.

No puedo ver ninguna fuga de memory obvia o cosas que pueda cambiar para ayudar a la recolección de basura. Guardo los identificadores de bloque en una matriz así que obviamente habrá un poco de memory pero esto no debería ser masivo. Es casi como si la API de file contiene todo el file que corta en la memory.

Está escrito como un service angular llamado desde un controller, creo que solo el código de service es pertinente:

(function() { 'use strict'; angular .module('app.core') .factory('blobUploadService', [ '$http', 'stringUtilities', blobUploadService ]); function blobUploadService($http, stringUtilities) { var defaultBlockSize = 1024 * 1024; // Default to 1024KB var stopWatch = {}; var state = {}; var initializeState = function(config) { var blockSize = defaultBlockSize; if (config.blockSize) blockSize = config.blockSize; var maxBlockSize = blockSize; var numberOfBlocks = 1; var file = config.file; var fileSize = file.size; if (fileSize < blockSize) { maxBlockSize = fileSize; } if (fileSize % maxBlockSize === 0) { numberOfBlocks = fileSize / maxBlockSize; } else { numberOfBlocks = parseInt(fileSize / maxBlockSize, 10) + 1; } return { maxBlockSize: maxBlockSize, numberOfBlocks: numberOfBlocks, totalBytesRemaining: fileSize, currentFilePointer: 0, blockIds: new Array(), blockIdPrefix: 'block-', bytesUploaded: 0, submitUri: null, file: file, baseUrl: config.baseUrl, sasToken: config.sasToken, fileUrl: config.baseUrl + config.sasToken, progress: config.progress, complete: config.complete, error: config.error, cancelled: false }; }; /* config: { baseUrl: // baseUrl for blob file uri (ie http://<accountName>.blob.core.windows.net/<container>/<blobname>), sasToken: // Shanetworking access signature querystring key/value prefixed with ?, file: // File object using the HTML5 File API, progress: // progress callback function, complete: // complete callback function, error: // error callback function, blockSize: // Use this to override the defaultBlockSize } */ var upload = function(config) { state = initializeState(config); var reader = new FileReader(); reader.onloadend = function(evt) { if (evt.target.readyState === FileReader.DONE && !state.cancelled) { // DONE === 2 var uri = state.fileUrl + '&comp=block&blockid=' + state.blockIds[state.blockIds.length - 1]; var requestData = new Uint8Array(evt.target.result); $http.put(uri, requestData, { headers: { 'x-ms-blob-type': 'BlockBlob', 'Content-Type': state.file.type }, transformRequest: [] }) .success(function(data, status, headers, config) { state.bytesUploaded += requestData.length; var percentComplete = ((parseFloat(state.bytesUploaded) / parseFloat(state.file.size)) * 100 ).toFixed(2); if (state.progress) state.progress(percentComplete, data, status, headers, config); uploadFileInBlocks(reader, state); }) .error(function(data, status, headers, config) { if (state.error) state.error(data, status, headers, config); }); } }; uploadFileInBlocks(reader, state); return { cancel: function() { state.cancelled = true; } }; }; function cancel() { stopWatch = {}; state.cancelled = true; return true; } function startStopWatch(handle) { if (stopWatch[handle] === undefined) { stopWatch[handle] = {}; stopWatch[handle].start = Date.now(); } } function stopStopWatch(handle) { stopWatch[handle].stop = Date.now(); var duration = stopWatch[handle].stop - stopWatch[handle].start; delete stopWatch[handle]; return duration; } var commitBlockList = function(state) { var uri = state.fileUrl + '&comp=blocklist'; var requestBody = '<?xml version="1.0" encoding="utf-8"?><BlockList>'; for (var i = 0; i < state.blockIds.length; i++) { requestBody += '<Latest>' + state.blockIds[i] + '</Latest>'; } requestBody += '</BlockList>'; $http.put(uri, requestBody, { headers: { 'x-ms-blob-content-type': state.file.type } }) .success(function(data, status, headers, config) { if (state.complete) state.complete(data, status, headers, config); }) .error(function(data, status, headers, config) { if (state.error) state.error(data, status, headers, config); // called asynchronously if an error occurs // or server returns response with an error status. }); }; var uploadFileInBlocks = function(reader, state) { if (!state.cancelled) { if (state.totalBytesRemaining > 0) { var fileContent = state.file.slice(state.currentFilePointer, state.currentFilePointer + state.maxBlockSize); var blockId = state.blockIdPrefix + stringUtilities.pad(state.blockIds.length, 6); state.blockIds.push(btoa(blockId)); reader.readAsArrayBuffer(fileContent); state.currentFilePointer += state.maxBlockSize; state.totalBytesRemaining -= state.maxBlockSize; if (state.totalBytesRemaining < state.maxBlockSize) { state.maxBlockSize = state.totalBytesRemaining; } } else { commitBlockList(state); } } }; return { upload: upload, cancel: cancel, startStopWatch: startStopWatch, stopStopWatch: stopStopWatch }; }; })(); 

¿Hay alguna manera de mover el scope de los objects para ayudar con Chrome GC? He visto a otras personas mencionar problemas similares pero entendió que Chromium había resuelto algo.

Debo decir que mi solución se basa en gran medida en la publicación del blog de Gaurav Mantri aquí:

http://gauravmantri.com/2013/02/16/uploading-large-files-in-windows-azure-blob-storage-using-shanetworking-access-signature-html-and-javascript/#comment-47480

No puedo ver ninguna fuga de memory obvia o cosas que pueda cambiar para ayudar a la recolección de basura. Guardo los identificadores de bloque en una matriz así que obviamente habrá un poco de memory pero esto no debería ser masivo. Es casi como si la API de file contiene todo el file que corta en la memory.

Estás en lo correcto. Los nuevos .slice() creados por .slice() se mantienen en la memory.

La solución es llamar a Blob.prototype.close() en la reference de Blob cuando se completa el Blob o el object de File .

Tenga en count también que en javascript at Question también se crea una nueva instancia de FileReader si se llama a la function de upload más de una vez.

4.3.1. El método de corte

El método slice() devuelve un nuevo object Blob con bytes que van desde el parámetro de start opcional hasta, pero sin include, el parámetro end opcional, y con un atributo de type que es el valor del parámetro contentType opcional.

Blob instancias de Blob existen durante la vida del document . Aunque Blob debería ser recolectado de basura una vez eliminado de Blob URL Store

9.6. Tiempo de vida de Blob URLs

Nota: Los agentes de usuario son libres de recolectar los resources eliminados del Blob URL Store .

Cada Blob debe tener un estado interno de instantánea , que debe establecerse inicialmente en el estado del almacenamiento subyacente, si existe dicho almacenamiento subyacente, y debe conservarse a través de StructunetworkingClone . Se puede encontrar otra definición normativa de estado de instantánea para File s.

4.3.2. El método cercano

Se dice que el método close() close una Blob y debe actuar de la siguiente manera:

  1. Si el readability state de readability state del object de context está CLOSED , finalice este algorithm.
  2. De lo contrario, establezca el readability state de readability state del context object en CLOSED .
  3. Si el object de context tiene una input en el Blob URL Store , elimine la input que corresponde al context object .

Si el object Blob se pasa a URL.createObjectURL() , llame a URL.revokeObjectURL() en Blob o en el object File , luego llame a .close() .

El método estático revokeObjectURL(url)

Revoca la Blob URL proporcionada en la url cadena eliminando la input correspondiente del Almacén de URL de Blob. Este método debe actuar de la siguiente manera: 1. Si la url refiere a una Blob que tiene un readability state de readability state CLOSED O si el valor proporcionado para el argumento url no es una Blob URL , O si el valor proporcionado para el argumento url no tiene una input en el Blob URL Store , esta llamada a método no hace nada. Los agentes de usuario pueden mostrar un post en la console de error. 2. De lo contrario, los agentes de usuario deben remove the entry del Blob URL Store para url .

Puede ver el resultado de estas llamadas abriendo

 chrome://blob-internals 

revisando los detalles de llamadas anteriores y posteriores que crean Blob y cierran Blob .

Por ejemplo, de

 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Refcount: 1 Content Type: text/plain Type: data Length: 3 

a

 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Refcount: 1 Content Type: text/plain 

siguiente llamada a .close() . Del mismo modo de

 blob:http://example.com/c2823f75-de26-46f9-a4e5-95f57b8230bd Uuid: 29e430a6-f093-40c2-bc70-2b6838a713bc 

Un enfoque alternativo podría ser enviar files como ArrayBuffer o fragments de búferes de matriz. Luego vuelva a ensamblar el file en el server.

O puede llamar FileReader constructor de FileReader , FileReader.prototype.readAsArrayBuffer() y load evento de FileReader cada vez.

En load evento de load de FileReader pase ArrayBuffer a Uint8Array , use ReadableStream , TypedArray.prototype.subarray() , .getReader() , .read() para get N fragments de ArrayBuffer como TypedArray en pull de Uint8Array . Cuando se .byteLength N fragments que igualan .byteLength of ArrayBuffer , pase la matriz de Uint8Array s al constructor Blob para recombinar las partes del file en un solo file en el browser; luego envíe Blob al server.

 <!DOCTYPE html> <html> <head> </head> <body> <input id="file" type="file"> <br> <progress value="0"></progress> <br> <output for="file"><img alt="preview"></output> <script type="text/javascript"> const [input, output, img, progress, fr, handleError, CHUNK] = [ document.querySelector("input[type='file']") , document.querySelector("output[for='file']") , document.querySelector("output img") , document.querySelector("progress") , new FileReader , (err) => console.log(err) , 1024 * 1024 ]; progress.addEventListener("progress", e => { progress.value = e.detail.value; e.detail.promise(); }); let [chunks, NEXT, CURR, url, blob] = [Array(), 0, 0]; input.onchange = () => { NEXT = CURR = progress.value = progress.max = chunks.length = 0; if (url) { URL.revokeObjectURL(url); if (blob.hasOwnProperty("close")) { blob.close(); } } if (input.files.length) { console.log(input.files[0]); progress.max = input.files[0].size; progress.step = progress.max / CHUNK; fr.readAsArrayBuffer(input.files[0]); } } fr.onload = () => { const VIEW = new Uint8Array(fr.result); const LEN = VIEW.byteLength; const {type, name:filename} = input.files[0]; const stream = new ReadableStream({ pull(controller) { if (NEXT < LEN) { controller .enqueue(VIEW.subarray(NEXT, !NEXT ? CHUNK : CHUNK + NEXT)); NEXT += CHUNK; } else { controller.close(); } }, cancel(reason) { console.log(reason); throw new Error(reason); } }); const [reader, processData] = [ stream.getReader() , ({value, done}) => { if (done) { return reader.closed.then(() => chunks); } chunks.push(value); return new Promise(resolve => { progress.dispatchEvent( new CustomEvent("progress", { detail:{ value:CURR += value.byteLength, promise:resolve } }) ); }) .then(() => reader.read().then(data => processData(data))) .catch(e => reader.cancel(e)) } ]; reader.read() .then(data => processData(data)) .then(data => { blob = new Blob(data, {type}); console.log("complete", data, blob); if (/image/.test(type)) { url = URL.createObjectURL(blob); img.onload = () => { img.title = filename; input.value = ""; } img.src = url; } else { input.value = ""; } }) .catch(e => handleError(e)) } </script> </body> </html> 

plnkr http://plnkr.co/edit/AEZ7iQce4QaJOKut71jk?p=preview


También puedes usar utilizar fetch()

 fetch(new Request("/path/to/server/", {method:"PUT", body:blob})) 

Para transmitir el cuerpo para una request de request , siga estos pasos:

  1. Deje que el cuerpo sea ​​el cuerpo de la request.
  2. Si el cuerpo es nulo, a continuación, ponga en queue una tarea de recuperación a petición para procesar la request de fin de cuerpo para la request y cancele estos pasos.

  3. Deja que leer sea ​​el resultado de leer un fragment de la stream del cuerpo .

    • Cuando se realiza la lectura con un object cuya propiedad done es falsa y cuya propiedad de value es un object Uint8Array , ejecute estas subpasos:

      1. Deje que los bytes sean la secuencia de bytes representada por el object Uint8Array .
      2. Transmitir bytes

      3. Aumenta los bytes transmitidos del cuerpo por la longitud de los bytes .

      4. Ejecute el paso anterior de nuevo.

    • Cuando se realiza la lectura con un object cuya propiedad done es verdadera, ponga en queue una tarea de recuperación a petición para procesar la request de fin de cuerpo para la request .

    • Cuando se realiza la lectura con un valor que no coincide con ninguno de los patrones anteriores, o se rechaza la lectura , finalice la recuperación en curso con el motivo fatal .

Ver también

  • Indicadores de progreso para fetch?

  • Captar con ReadableStream