我有一个需要将大文件上传到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的博客文章:
答案 0 :(得分:4)
我看不到任何明显的内存泄漏或我可以改变以帮助的事情 垃圾收集。我很明显地将块ID存储在一个数组中 会有一些内存爬行,但这不应该是巨大的。它的 几乎就像File API持有它切入的整个文件一样 存储器中。
你是对的。由Blob
创建的新.slice()
被保存在内存中。
解决方案是在处理Blob.prototype.close()
或Blob
对象完成时,在Blob
引用上调用File
。
另请注意,如果javascript
函数被多次调用,则问题FileReader
也会创建upload
的新实例。
slice()
方法返回一个具有字节范围的新Blob
对象 从可选的start
参数到但不包括 可选的end
参数,以及type
属性 可选contentType
参数的值。
Blob
个实例存在document
的生命周期。虽然从Blob
Blob URL Store
后应该进行垃圾回收
注意:用户代理可以自由地从中删除垃圾收集资源
Blob URL Store
。
每个
Blob
必须有一个内部 snapshot state ,必须是 最初设置为底层存储的状态,如果有的话 底层存储存在,必须通过保存StructuredClone
。快照状态的进一步规范定义可以 找到File
s。
close()
方法被称为close
和Blob
,并且必须充当 如下:
- 如果上下文对象的
readability state
为CLOSED
,请终止此算法。- 否则,请将
readability state
的{{1}}设置为context object
。- 如果上下文对象在
醇>Blob URL Store
中有条目,请删除与CLOSED
对应的条目。
如果将context object
对象传递给Blob
,请致电URL.createObjectURL()
或URL.revokeObjectURL()
对象上的Blob
,然后致电File
。
通过从Blob网址存储中删除相应的条目来撤消字符串
.close()
中提供的Blob URL
。这种方法必须采取行动 如下: 1.如果url
引用url
Blob
readability state
或CLOSED
为url
参数提供的值, 不是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()
构造函数,load
和FileReader
load
事件。
在FileReader
ArrayBuffer
次Uint8Array
次ReadableStream
次TypedArray.prototype.subarray()
次事件中,使用.getReader()
,.read()
,N
,{{ 1}}从ArrayBuffer
TypedArray
获取pull
Uint8Array
个N
块。当.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 ,请运行这些 步骤进行:
- 让正文成为请求request。
如果 body 为null,则在请求上对获取任务进行排队,以处理请求的请求体端请求中止这些步骤。
- 醇>
让读取是从 body的流中读取一个块的结果。
使用
done
属性为false且其value
属性为Uint8Array
对象的对象实现 read 时,运行这些 子步骤:
- 让 bytes 成为
Uint8Array
对象表示的字节序列。传输字节。
按字节的长度增加正文传输的字节。
再次运行上述步骤。
使用
done
属性为true的对象实现 read 时,在请求上排队获取任务以处理请求结束身体 request 。如果 read 的值与上述两种模式都不匹配,或者 read 被拒绝,则终止正在进行的操作 获取原因致命。
另见