Chrome内存问题 - 文件API + AngularJS

Wil*_*llH 11 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
    };
};
})();
Run Code Online (Sandbox Code Playgroud)

我有什么方法可以移动对象的范围来帮助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

gue*_*314 4

\n

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

\n
\n\n

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

\n\n

解决方案是在处理或对象完成时Blob.prototype.close()调用引用。BlobBlobFile

\n\n

另请注意,at at Question 还会创建if函数被多次调用的javascript新实例。FileReaderupload

\n\n
\n

4.3.1. 切片法

\n\n

slice()方法返回一个新Blob对象,其字节范围为\n 从可选start参数到\n 但不包括\n 可选end参数,并且type属性为\n 可选contentType参数的值。

\n
\n\n

Blob实例在 的生命周期中都存在document。虽然Blob一旦从其中删除就应该被垃圾收集Blob URL Store

\n\n
\n

9.6。Blob URL 的生命周期

\n\n

注意:用户代理可以自由地垃圾收集从Blob URL Store.

\n
\n\n

\n\n

\n

每个都Blob必须有一个内部快照状态,该状态必须最初设置为底层存储的状态(如果存在任何此类底层存储),并且必须通过底层存储进行保留 StructuredClone。可以为 s 找到快照状态的进一步规范定义File

\n
\n\n

\n\n

\n

4.3.2. 关闭方法

\n\n

close()方法据说是close为了Blob,并且必须按如下方式运行:

\n\n
    \n
  1. 如果readability state上下文对象的CLOSED,则终止该算法。
  2. \n
  3. 否则,将 的readability state设置context objectCLOSED
  4. \n
  5. 如果上下文对象在 中有条目Blob URL Store,则删除对应于 的条目context object
  6. \n
\n
\n\n

如果将Blob对象传递给URL.createObjectURL(),则调用URL.revokeObjectURL()Blob对象File,然后调用.close()

\n\n
\n

revokeObjectURL(url)方法

\n\n
\n

通过从 Blob URL 存储中删除相应的条目来撤销Blob URL字符串中提供的内容。url此方法必须按如下方式执行\n:\n 1. 如果 引用url具有Blobareadability state的a CLOSED,或者如果为参数提供的值url不是 a Blob URL,或者如果为参数提供的值url不具有 a中的条目Blob URL Store,此方法调用不执行任何操作。用户代理可能会在错误控制台上显示一条消息。\n 2. 否则,用户代理必须remove the entryBlob URL Storeforurl .

\n
\n
\n\n

您可以通过打开查看这些调用的结果

\n\n
chrome://blob-internals \n
Run Code Online (Sandbox Code Playgroud)\n\n

Blob查看创建和关闭呼叫之前和之后的详细信息Blob

\n\n

例如,从

\n\n
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\nRefcount: 1\nContent Type: text/plain\nType: data\nLength: 3\n
Run Code Online (Sandbox Code Playgroud)\n\n

\n\n
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\nRefcount: 1\nContent Type: text/plain\n
Run Code Online (Sandbox Code Playgroud)\n\n

以下致电.close(). 同样来自

\n\n
blob:http://example.com/c2823f75-de26-46f9-a4e5-95f57b8230bd\nUuid: 29e430a6-f093-40c2-bc70-2b6838a713bc\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

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

\n\n

或者您可以调用每个FileReader构造函数、FileReader.prototype.readAsArrayBuffer()load事件FileReader一次。

\n\n

在传递toload的情况下,使用, , ,来获取作为at from的块。当处理完等于 的块时,将 s 数组传递给构造函数,以在浏览器中将文件部分重新组合成单​​个文件;然后发送 到服务器。FileReaderArrayBufferUint8ArrayReadableStreamTypedArray.prototype.subarray().getReader().read()NArrayBufferTypedArraypullUint8ArrayN.byteLengthArrayBufferUint8ArrayBlobBlob

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

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

\n\n
\n\n

您还可以使用利用fetch()

\n\n
fetch(new Request("/path/to/server/", {method:"PUT", body:blob}))\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n

传输请求 request的正文,请运行以下\n步骤:

\n\n
    \n
  1. body为 request\xe2\x80\x99s body
  2. \n
  3. 如果body为 null,则根据请求对获取任务进行排队,以处理请求的请求主体末尾并中止这些步骤。

  4. \n
  5. read为从body\xe2\x80\x99s流中读取块的结果。

    \n\n
      \n
    • 当使用属性为 false 且属性是对象的对象完成读取时,运行以下子步骤:donevalueUint8Array

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

      4. \n
      5. body\xe2\x80\x99s传输的字节数增加bytes\xe2\x80\x99s长度。

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

      8. \n
    • \n
    • 当使用属性为 true 的对象完成读取时,根据请求done对获取任务进行排队,以处理request的请求正文\n 。

    • \n
    • 读取满足与上述模式都不匹配的值时,或者读取被拒绝时,终止正在进行的读取,原因为fatal

    • \n
  6. \n
\n
\n\n

也可以看看

\n\n\n