Работник, блокирующий поток пользовательского интерфейса в 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 или что у вас) содержимого файла обратно в главный поток для подписи; это гарантировало бы, что чтение фрагмента не происходит в главном потоке.

Другие вопросы по тегам