Работник, блокирующий поток пользовательского интерфейса в Chrome
Я создаю веб-приложение, которое использует EvaporateJS для загрузки больших файлов в Amazon S3 с помощью Multipart Uploads. Я заметил проблему, когда каждый раз при запуске нового блока браузер зависал на ~2 секунды. Я хочу, чтобы пользователь мог продолжать использовать мое приложение во время загрузки, и это замораживание делает это плохим опытом.
Я использовал Chrome Timeline, чтобы выяснить причину этого и обнаружил, что это хэширование SparkMD5. Итак, я переместил весь процесс загрузки в Worker, который, я думал, решит проблему.
Ну, теперь проблема исправлена в Edge и Firefox, но Chrome все еще имеет ту же проблему.
Вот скриншот моей временной шкалы:
Как вы можете видеть, во время зависаний мой основной поток практически ничего не делает, с JavaScript, работающим в течение этого времени. Вся работа выполняется в моем рабочем потоке, и даже она работает всего около 600 мс или около того, а не 1386 мс, которые принимает мой кадр.
Я действительно не уверен, что является причиной проблемы, есть ли какие-то ошибки с работниками, о которых я должен знать?
Вот код для моего работника:
var window = self; // For Worker-unaware scripts
// Shim to make Evaporate work in a Worker
var document = {
createElement: function() {
var href = undefined;
var elm = {
set href(url) {
var obj = new URL(url);
elm.protocol = obj.protocol;
elm.hostname = obj.hostname;
elm.pathname = obj.pathname;
elm.port = obj.port;
elm.search = obj.search;
elm.hash = obj.hash;
elm.host = obj.host;
href = url;
},
get href() {
return href;
},
protocol: undefined,
hostname: undefined,
pathname: undefined,
port: undefined,
search: undefined,
hash: undefined,
host: undefined
};
return elm;
}
};
importScripts("/lib/sha256/sha256.min.js");
importScripts("/lib/spark-md5/spark-md5.min.js");
importScripts("/lib/url-parse/url-parse.js");
importScripts("/lib/xmldom/xmldom.js");
importScripts("/lib/evaporate/evaporate.js");
DOMParser = self.xmldom.DOMParser;
var defaultConfig = {
computeContentMd5: true,
cryptoMd5Method: function (data) { return btoa(SparkMD5.ArrayBuffer.hash(data, true)); },
cryptoHexEncodedHash256: sha256,
awsSignatureVersion: "4",
awsRegion: undefined,
aws_url: "https://s3-ap-southeast-2.amazonaws.com",
aws_key: undefined,
customAuthMethod: function(signParams, signHeaders, stringToSign, timestamp, awsRequest) {
return new Promise(function(resolve, reject) {
var signingRequestId = currentSigningRequestId++;
postMessage(["signingRequest", signingRequestId, signParams.videoId, timestamp, awsRequest.signer.canonicalRequest()]);
queuedSigningRequests[signingRequestId] = function(signature) {
queuedSigningRequests[signingRequestId] = undefined;
if(signature) {
resolve(signature);
} else {
reject();
}
}
});
},
//logging: false,
bucket: undefined,
allowS3ExistenceOptimization: false,
maxConcurrentParts: 5
}
var currentSigningRequestId = 0;
var queuedSigningRequests = [];
var e = undefined;
var filekey = undefined;
onmessage = function(e) {
var messageType = e.data[0];
switch(messageType) {
case "init":
var globalConfig = {};
for(var k in defaultConfig) {
globalConfig[k] = defaultConfig[k];
}
for(var k in e.data[1]) {
globalConfig[k] = e.data[1][k];
}
var uploadConfig = e.data[2];
Evaporate.create(globalConfig).then(function(evaporate) {
var e = evaporate;
filekey = globalConfig.bucket + "/" + uploadConfig.name;
uploadConfig.progress = function(p, stats) {
postMessage(["progress", p, stats]);
};
uploadConfig.complete = function(xhr, awsObjectKey, stats) {
postMessage(["complete", xhr, awsObjectKey, stats]);
}
uploadConfig.info = function(msg) {
postMessage(["info", msg]);
}
uploadConfig.warn = function(msg) {
postMessage(["warn", msg]);
}
uploadConfig.error = function(msg) {
postMessage(["error", msg]);
}
e.add(uploadConfig);
});
break;
case "pause":
e.pause(filekey);
break;
case "resume":
e.resume(filekey);
break;
case "cancel":
e.cancel(filekey);
break;
case "signature":
var signingRequestId = e.data[1];
var signature = e.data[2];
queuedSigningRequests[signingRequestId](signature);
break;
}
}
Обратите внимание, что он полагается на вызывающий поток, предоставляя ему открытый ключ AWS, имя корзины AWS и регион AWS, ключ объекта AWS и объект входного файла, которые все представлены в сообщении 'init'. Когда ему нужно что-то подписать, он отправляет сообщение 'signatureRequest' в родительский поток, который, как ожидается, предоставит подпись в сообщении 'signature', как только оно будет получено из конечной точки подписи моего API.
1 ответ
Я не могу привести очень хороший пример или проанализировать, что вы делаете, используя только код Worker, но я сильно подозреваю, что проблема связана либо с чтением фрагмента в главном потоке, либо с какой-то неожиданной обработкой, которую вы выполняете. делаю на чанке в главном потоке. Может быть, опубликовать основной код потока, который вызывает postMessage
к работнику?
Если бы я отлаживал это прямо сейчас, я бы попытался переместить ваш FileReader
операции в работника. Если вы не возражаете против блокировки Worker во время загрузки чанка, вы также можете использовать FileReaderSync
,
Обновление после комментариев
Требуется ли для создания предварительно назначенного URL-адреса хэширование содержимого файла + метаданные + ключ? Хэширование содержимого файла займет O(n) в размере чанка, и это возможно, если хеш является первой операцией, которая читает из Blob
, что загрузка содержимого файла может быть отложена до начала хеширования. Если вы не вынуждены хранить подпись в главном потоке (вы не доверяете работнику ключевой материал?), Это было бы еще одной хорошей вещью для работника.
Если перемещение подписи в Worker слишком много, вы можете попросить его сделать что-то, чтобы заставить Blob
читать и / или передать ArrayBuffer
(или же Uint8Array
или что у вас) содержимого файла обратно в главный поток для подписи; это гарантировало бы, что чтение фрагмента не происходит в главном потоке.