Chrome 内存问题 - 文件 API + AngularJS [英] Chrome memory issue - File API + AngularJS
问题描述
我有一个需要将大文件上传到 Azure BLOB 存储的 Web 应用程序.我的解决方案使用 HTML5 文件 API 切片成块,然后将其作为 blob 块放置,块的 ID 存储在数组中,然后将块作为 blob 提交.
该解决方案在 IE 中运行良好.在 64 位 Chrome 上,我已成功上传 4Gb 文件,但内存使用量非常大(2Gb+).在 32 位 Chrome 上,特定的 chrome 进程将达到 500-550Mb 左右,然后崩溃.
我看不到任何明显的内存泄漏或我可以更改以帮助垃圾收集的内容.我将块 ID 存储在一个数组中,所以显然会有一些内存溢出,但这不应该是大量的.这几乎就像 File API 保存了它切入内存的整个文件.
它是作为从控制器调用的 Angular 服务编写的,我认为只有服务代码是相关的:
(function() {'使用严格';有角的.module('app.core').factory('blobUploadService',['$http', 'stringUtilities',blob上传服务]);函数 blobUploadService($http, stringUtilities) {var defaultBlockSize = 1024 * 1024;//默认为 1024KBvar 秒表 = {};var 状态 = {};var initializeState = 函数(配置){var blockSize = defaultBlockSize;如果(config.blockSize)blockSize = config.blockSize;var maxBlockSize = blockSize;var numberOfBlocks = 1;var 文件 = config.file;var fileSize = file.size;如果(文件大小<块大小){maxBlockSize = 文件大小;}if (fileSize % maxBlockSize === 0) {numberOfBlocks = fileSize/maxBlockSize;} 别的 {numberOfBlocks = parseInt(fileSize/maxBlockSize, 10) + 1;}返回 {最大块大小:最大块大小,numberOfBlocks: numberOfBlocks,totalBytesRemaining:文件大小,当前文件指针:0,blockIds:新数组(),blockIdPrefix: 'block-',字节已上传:0,提交Uri:空,文件:文件,baseUrl: config.baseUrl,sasToken: config.sasToken,fileUrl: config.baseUrl + config.sasToken,进度:config.progress,完成:config.complete,错误:config.error,取消:假};};/* 配置:{baseUrl://blob 文件 uri 的 baseUrl(即 http://<accountName>.blob.core.windows.net/<container>/<blobname>),sasToken://以?为前缀的共享访问签名查询字符串键/值,file://使用 HTML5 文件 API 的文件对象,progress://进度回调函数,complete://完成回调函数,error://错误回调函数,blockSize://使用它来覆盖 defaultBlockSize} */var 上传 = 函数(配置){状态 = 初始化状态(配置);var reader = new FileReader();reader.onloadend = 函数(evt){if (evt.target.readyState === FileReader.DONE && !state.cancelled) {//完成 === 2var uri = state.fileUrl + '&comp=block&blockid=' + state.blockIds[state.blockIds.length - 1];var requestData = new Uint8Array(evt.target.result);$http.put(uri,请求数据,{标题:{'x-ms-blob-type': 'BlockBlob','内容类型':state.file.type},转换请求:[]}).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);}).错误(功能(数据,状态,标题,配置){if (state.error) state.error(data, status, headers, config);});}};uploadFileInBlocks(reader, state);返回 {取消:函数(){state.cancelled = true;}};};函数取消(){秒表 = {};state.cancelled = true;返回真;}功能开始停止手表(句柄){if (stopWatch[handle] === undefined) {秒表[句柄] = {};stopWatch[handle].start = Date.now();}}功能停止StopWatch(句柄){stopWatch[handle].stop = Date.now();var duration = stopWatch[handle].stop - stopWatch[handle].start;删除秒表[句柄];返回时长;}var commitBlockList = 函数(状态){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 += '<最新>'+ state.blockIds[i] + '最新>';}requestBody += '</BlockList>';$http.put(uri,请求体,{标题:{x-ms-blob-content-type":state.file.type}}).success(function(data, status, headers, config) {if (state.complete) state.complete(data, status, headers, config);}).错误(功能(数据,状态,标题,配置){if (state.error) state.error(data, status, headers, config);//发生错误时异步调用//或者服务器返回带有错误状态的响应.});};var uploadFileInBlocks = function(reader, state) {如果(!状态.取消){如果(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;如果(state.totalBytesRemaining < state.maxBlockSize){state.maxBlockSize = state.totalBytesRemaining;}} 别的 {commitBlockList(状态);}}};返回 {上传:上传,取消:取消,startStopWatch:startStopWatch,停止秒表:停止秒表};};})();
有什么方法可以移动对象的范围来帮助 Chrome GC?我看到其他人提到了类似的问题,但我知道 Chromium 已经解决了一些问题.
我应该说我的解决方案很大程度上基于 Gaurav Mantri 的博客文章:
我看不到任何明显的内存泄漏或我可以更改以提供帮助的内容垃圾收集.我很明显地将块 ID 存储在一个数组中会有一些内存蠕变,但这不应该是大量的.它是几乎就好像 File API 保存了它切入的整个文件记忆.
你说得对.由 .slice()
创建的新 Blob
保存在内存中.
解决方法是在处理Blob
或File时,在
Blob
引用上调用Blob.prototype.close()
代码>对象完成.
另请注意,如果 upload
函数被多次调用,则在 javascript
处的 Question 还会创建一个 FileReader
的新实例.
slice()
方法返回一个新的 Blob
对象,其字节范围为从可选的 start
参数到但不包括可选的 end
参数,并带有一个 type
属性,它是可选的 contentType
参数的值.
Blob
实例存在于 document
的生命周期中.尽管 Blob
一旦从 Blob URL Store
注意:用户代理可以自由地垃圾收集从Blob URL 存储
.
每个Blob
必须有一个内部快照状态,必须是最初设置为底层存储的状态,如果有的话底层存储存在,必须通过StructuredClone
.快照状态的进一步规范定义可以可以找到 File
s.
close()
方法被称为 close
一个 Blob
,并且必须充当如下:
- 如果上下文对象的
可读性状态
是CLOSED
,终止该算法. - 否则,将
上下文对象
的可读性状态
设置为CLOSED
. - 如果上下文对象在
Blob URL Store
,删除对应于context object
的条目.
如果Blob
对象被传递给URL.createObjectURL()
,则在Blob
上调用URL.revokeObjectURL()
> 或 File
对象,然后调用 .close()
.
revokeObjectURL(url)
静态方法
撤销Blob URL
string url
通过从 Blob URL 存储中删除相应的条目.这个方法必须起作用如下:1. 如果 url
指的是一个 Blob
的 readability state
为 CLOSED
或者如果为 url
参数提供的值是不是 Blob URL
,或者如果为 url
参数提供的值确实Blob URL Store
中没有条目,此方法调用没有没有.用户代理可能会在错误控制台上显示一条消息.2. 否则,用户代理必须删除条目
Blob URL Store
用于 url
.
您可以通过打开
来查看这些调用的结果chrome://blob-internals
查看创建 Blob
和关闭 Blob
的调用前后的详细信息.
例如来自
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx引用数:1内容类型:文本/纯文本类型:数据长度:3
到
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx引用数:1内容类型:文本/纯文本
在调用 .close()
之后.同样来自
blob:http://example.com/c2823f75-de26-46f9-a4e5-95f57b8230bdUuid:29e430a6-f093-40c2-bc70-2b6838a713bc
<小时>
另一种方法是将文件作为 ArrayBuffer
或数组缓冲区块发送.然后在服务器上重新组装文件.
或者你可以调用FileReader
构造函数、FileReader.prototype.readAsArrayBuffer()
和FileReader
的load
事件> 每一次.
在FileReader
的load
事件将ArrayBuffer
传递给Uint8Array
,使用ReadableStream
, TypedArray.prototype.subarray()
, .getReader()
, .read()
获取N
块ArrayBuffer
作为 TypedArray
在 pull
从 Uint8Array
.当 N
个块等于 ArrayBuffer
的 .byteLength
处理完后,将 Uint8Array
的数组传递给 Blob
构造函数,用于在浏览器中将文件部分重新组合成单个文件;然后将 Blob
发送到服务器.
<头>头部><身体><输入id=文件"类型=文件"><br><progress value="0"></progress><br><output for="file"><img alt="preview"></output><script type="text/javascript">const [输入,输出,img,进度,fr,handleError,CHUNK] = [document.querySelector("input[type='file']"), document.querySelector("output[for='file']"), document.querySelector("输出图片"), document.querySelector("进度"), 新文件阅读器, (错误) =>控制台日志(错误), 1024 * 1024];progress.addEventListener("progress", e => {progress.value = e.detail.value;e.detail.promise();});让 [chunks, NEXT, CURR, url, blob] = [Array(), 0, 0];input.onchange = () =>{NEXT = CURR =progress.value =progress.max = chunks.length = 0;如果(网址){URL.revokeObjectURL(url);如果(blob.hasOwnProperty(关闭")){blob.close();}}如果(输入.文件.长度){控制台日志(输入文件[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 流 = 新可读流({拉(控制器){如果(下一个< LEN){控制器.enqueue(VIEW.subarray(NEXT, !NEXT ? CHUNK : CHUNK + NEXT));下一个 += 块;} 别的 {控制器关闭();}},取消(原因){控制台日志(原因);抛出新的错误(原因);}});const [读者,进程数据] = [流.getReader(), ({value, done}) =>{如果(完成){返回 reader.closed.then(() => chunks);}块.推(值);返回新的承诺(解决 => {progress.dispatchEvent(新自定义事件(进度",{细节:{value:CURR += value.byteLength,承诺:解决}}));}).then(() => reader.read().then(data => processData(data))).catch(e => reader.cancel(e))}];读者阅读().then(data => processData(data)).then(数据=> {blob = new Blob(data, {type});控制台日志(完成",数据,blob);如果(/图像/.测试(类型)){url = URL.createObjectURL(blob);img.onload = () =>{img.title = 文件名;input.value = "";}img.src = url;} 别的 {input.value = "";}}).catch(e => handleError(e))}