Chrome内存问题 - 文件API + AngularJS

时间:2017-01-03 09:28:51

标签: javascript html5 google-chrome azure fileapi

我有一个需要将大文件上传到Azure BLOB存储的Web应用程序。我的解决方案使用HTML5 File API切片成块,然后将块作为blob块放置,块的ID存储在数组中,然后块作为blob提交。

该解决方案在IE中运行良好。在64位Chrome上,我已经成功上传了4Gb文件但是看到了非常大的内存使用量(2Gb +)。在32位Chrome上,特定的镀铬工艺将达到约500-550Mb然后崩溃。

我看不到任何明显的内存泄漏或我可以更改以帮助垃圾回收的事情。我将块ID存储在一个数组中,所以显然会有一些内存蠕变,但这不应该很大。这几乎就像File API将整个文件保存在内存中一样。

它是作为一个从控制器调用的Angular服务编写的,我认为只是服务代码是相关的:

(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 (i.e. http://<accountName>.blob.core.windows.net/<container>/<blobname>),
      sasToken: // Shared 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
    };
};
})();

我有什么方法可以移动对象的范围来帮助Chrome GC吗?我见过其他人提到类似的问题,但是知道Chromium解决了一些问题。

我应该说我的解决方案主要基于Gaurav Mantri的博客文章:

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

1 个答案:

答案 0 :(得分:4)

  

我看不到任何明显的内存泄漏或我可以改变以帮助的事情   垃圾收集。我很明显地将块ID存储在一个数组中   会有一些内存爬行,但这不应该是巨大的。它的   几乎就像File API持有它切入的整个文件一样   存储器中。

你是对的。由Blob创建的新.slice()被保存在内存中。

解决方案是在处理Blob.prototype.close()Blob对象完成时,在Blob引用上调用File

另请注意,如果javascript函数被多次调用,则问题FileReader也会创建upload的新实例。

  

4.3.1. The slice method

     

slice()方法返回一个具有字节范围的新Blob对象   从可选的start参数到但不包括   可选的end参数,以及type属性   可选contentType参数的值。

Blob个实例存在document的生命周期。虽然从Blob

中删除Blob URL Store后应该进行垃圾回收
  

9.6. Lifetime of Blob URLs

     

注意:用户代理可以自由地从中删除垃圾收集资源   Blob URL Store

  

每个Blob必须有一个内部 snapshot state ,必须是   最初设置为底层存储的状态,如果有的话   底层存储存在,必须通过保存   StructuredClone。快照状态的进一步规范定义可以   找到File s。

  

4.3.2. The close method

     

close()方法被称为closeBlob,并且必须充当   如下:

     
      
  1. 如果上下文对象的readability stateCLOSED,请终止此算法。
  2.   
  3. 否则,请将readability state的{​​{1}}设置为context object
  4.   
  5. 如果上下文对象在Blob URL Store中有条目,请删除与CLOSED对应的条目。
  6.   

如果将context object对象传递给Blob,请致电URL.createObjectURL()URL.revokeObjectURL()对象上的Blob,然后致电File

  

revokeObjectURL(url)静态方法

     
    

通过从Blob网址存储中删除相应的条目来撤消字符串.close()中提供的Blob URL。这种方法必须采取行动     如下:     1.如果url引用url Blob readability stateCLOSEDurl参数提供的值,     不是Blob URL,或者如果为url参数提供的值那么     这个方法调用没有Blob URL Store中的条目     没有。用户代理可以在错误控制台上显示消息。     2.否则,用户代理必须Blob URL Store url chrome://blob-internals 。{/ p>   

您可以通过打开

来查看这些来电的结果
Blob

审核创建Blob并关闭xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Refcount: 1 Content Type: text/plain Type: data Length: 3 的来电之前和之后的详细信息。

例如,来自

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

.close()

致电blob:http://example.com/c2823f75-de26-46f9-a4e5-95f57b8230bd Uuid: 29e430a6-f093-40c2-bc70-2b6838a713bc 后。同样来自

ArrayBuffer

另一种方法可以是将文件作为FileReader或数组缓冲区块发送。然后在服务器上重新组装文件。

或者您可以每次致电FileReader.prototype.readAsArrayBuffer()构造函数,loadFileReader load事件。

FileReader ArrayBufferUint8ArrayReadableStreamTypedArray.prototype.subarray()次事件中,使用.getReader().read()N,{{ 1}}从ArrayBuffer TypedArray获取pull Uint8ArrayN块。当.byteLength块等于ArrayBuffer的{​​{1}}时,将Uint8Array的数组传递给Blob构造函数,以便在浏览器中将文件部分重新组合为单个文件;然后将Blob发送到服务器。

<!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 remove the entry

您还可以使用利用fetch()

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

http://plnkr.co/edit/AEZ7iQce4QaJOKut71jk?p=preview 请求 transmit body ,请运行这些   步骤进行:

     
      
  1. 正文成为请求request
  2.   
  3. 如果 body 为null,则在请求上对获取任务进行排队,以处理请求的请求体端请求中止这些步骤。

  4.   
  5. 读取是从 body的流中读取一个块的结果。

         
        
    • 使用done属性为false且其value属性为Uint8Array对象的对象实现 read 时,运行这些   子步骤:

           
          
      1. bytes 成为Uint8Array对象表示的字节序列。
      2.   
      3. 传输字节

      4.   
      5. 字节的长度增加正文传输的字节。

      6.   
      7. 再次运行上述步骤。

      8.   
    •   
    • 使用done属性为true的对象实现 read 时,在请求上排队获取任务以处理请求结束身体    request

    •   
    • 如果 read 的值与上述两种模式都不匹配,或者 read 被拒绝,则终止正在进行的操作   获取原因致命

    •   
  6.   

另见