Используйте API Fetch Streams для асинхронного использования фрагментированных данных без использования рекурсии.

Я использую JavaScript fetch API потоков для использовать фрагментированный JSON асинхронно, как в этом ответе.

Мое приложение может получать до 25 небольших объектов JSON в секунду (по одному на каждый кадр видео) в течение часа.

Когда входящие фрагменты большие (более 1000 объектов JSON на фрагмент), мой код работает хорошо — быстро, с минимальным использованием памяти — он может легко надежно получить 1 000 000 объектов JSON.

Когда входящие фрагменты меньше (5 объектов JSON на фрагмент), мой код работает плохо - медленно, много потребляет памяти. Браузер умирает примерно на 50 000 объектов JSON.

После большой отладки в инструментах разработчика оказалось, что проблема заключается в рекурсивном характере кода.

Я пытался удалить рекурсию, но кажется, что это необходимо, потому что API зависит от моего кода, возвращающего обещание в цепочку?!

Как удалить эту рекурсию или использовать что-то другое, кроме fetch?


Код с рекурсией (работает)

String.prototype.replaceAll = function(search, replacement) {
    var target = this;
    return target.replace(new RegExp(search, 'g'), replacement);
};

results = []

fetch('http://localhost:9999/').then(response => {
    const reader = response.body.getReader();
    td = new TextDecoder("utf-8");
    buffer = "";

    reader.read().then(function processText({ done, value }) {
        if (done) {
          console.log("Stream done.");
          return;
        }

        try {
            decoded = td.decode(value);
            buffer += decoded;
            if (decoded.length != 65536){
                toParse = "["+buffer.trim().replaceAll("\n",",")+"]";
                result = JSON.parse(toParse);
                results.push(...result);
                console.log("Received " + results.length.toString() + " objects")
                buffer = "";
            }
        }
        catch(e){
            // Doesn't need to be reported, because partial JSON result will be parsed next time around (from buffer).
            //console.log("EXCEPTION:"+e);
        }

        return reader.read().then(processText);
    })
});

Код без рекурсии (не работает)

String.prototype.replaceAll = function(search, replacement) {
    var target = this;
    return target.replace(new RegExp(search, 'g'), replacement);
};

results = []
finished = false

fetch('http://localhost:9999/').then(response => {
    const reader = response.body.getReader();
    td = new TextDecoder("utf-8");
    buffer = "";
    lastResultSize = -1

    while (!finished)
        if (lastResultSize < results.length)
        {
            lastResultSize = results.length;
            reader.read().then(function processText({ done, value }) {

                if (done) {
                  console.log("Stream done.");
                  finished = true;
                  return;
                }
                else
                    try {
                        decoded = td.decode(value);
                        //console.log("Received chunk " + decoded.length.toString() + " in length");
                        buffer += decoded;
                        if (decoded.length != 65536){
                            toParse = "["+buffer.trim().replaceAll("\n",",")+"]";
                            result = JSON.parse(toParse);
                            results.push(...result);
                            console.log("Received " + results.length.toString() + " objects")
                            buffer = "";
                            //console.log("Parsed chunk " + toParse.length.toString() + " in length");
                        }
                    }
                    catch(e) {
                        // Doesn't need to be reported, because partial JSON result will be parsed next time around (from buffer).
                        //console.log("EXCEPTION:"+e);
                    }
            })
        }
});

Для полноты вот код Python, который я использую на тестовом сервере. Обратите внимание на строку, содержащую sleep, которая меняет поведение фрагментации:

import io
import urllib
import inspect
from http.server import HTTPServer,BaseHTTPRequestHandler
from time import sleep


class TestServer(BaseHTTPRequestHandler):

    def do_GET(self):
        args = urllib.parse.parse_qs(self.path[2:])
        args = {i:args[i][0] for i in args}
        response = ''

        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Transfer-Encoding', 'chunked')
        self.end_headers()

        for i in range (1000000):
            self.wfile.write(bytes(f'{{"x":{i}, "text":"fred!"}}\n','utf-8'))
            sleep(0.001)  # Comment this out for bigger chunks sent to the client!

def main(server_port:"Port to serve on."=9999,server_address:"Local server name."=''):
    httpd = HTTPServer((server_address, server_port), TestServer)
    print(f'Serving on http://{httpd.server_name}:{httpd.server_port} ...')
    httpd.serve_forever()


if __name__ == '__main__':
    main()

person QA Collective    schedule 12.10.2018    source источник
comment
Вы пробовали это в других браузерах и наблюдаете ли вы такое же поведение в других браузерах?   -  person sideshowbarker    schedule 12.10.2018
comment
Я пробовал это как в Firefox, так и в Chrome. Они оба ведут себя одинаково, хотя и с разными профилями производительности, например. Firefox работает дольше, но медленнее, Chrome работает быстрее, но получает меньше объектов JavaScript.   -  person QA Collective    schedule 13.10.2018
comment
Возможно, стоит зарегистрировать ошибки браузера, чтобы получить от них представление об их реализации и о том, ограничивает ли природа требований их вообще для лучшей оптимизации для этого сценария. В худшем случае ответ о том, что они знают об этом, может быть проблемой для разработчиков, но они не планируют менять свои реализации. В лучшем случае они соглашаются, что такой сценарий должен быть возможен, и изменяют свои реализации, чтобы в этом случае браузеру не хватило памяти и не произошел сбой.   -  person sideshowbarker    schedule 13.10.2018
comment
Я не уверен, что источником проблемы является сам браузер - рекурсия консервативной глубины все еще в порядке в браузере. Моя главная проблема заключается в том, что я не вижу, как реализовать итеративную версию этого алгоритма, и действительно кажется, что API Fetch Streams был написан с учетом рекурсии как в лучшем случае, когда в моем случае это не так. Поскольку у меня здесь мало активности, я мог бы попробовать рабочую группу Streams API (github.com/whatwg/streams) и укажите их здесь. Спасибо за QA и идеи :)   -  person QA Collective    schedule 15.10.2018


Ответы (1)


Часть, которую вам не хватает, заключается в том, что функция, переданная .then(), всегда вызывается асинхронно, то есть с пустым стеком. Таким образом, здесь нет реальной рекурсии. Вот почему ваша версия «без рекурсии» не работает.

Простое решение этой проблемы — использовать асинхронные функции и оператор ожидания. Если вы вызываете read() следующим образом:

const {value, done} = await reader.read();

... тогда вы можете вызвать его в цикле, и он будет работать так, как вы ожидаете.

Я не знаю конкретно, где у вас утечка памяти, но использование вами глобальных переменных выглядит как проблема. Я рекомендую вам всегда ставить 'use strict'; в начало вашего кода, чтобы компилятор уловил эти проблемы за вас. Затем используйте let или const всякий раз, когда вы объявляете переменную.

Я рекомендую вам использовать TextDecoderStream, чтобы избежать проблем, когда символ разбит на несколько фрагментов. У вас также будут проблемы, когда объект JSON разделен на несколько фрагментов.

См. раздел Добавить демо-версию дочернего потока с возможностью записи, чтобы узнать, как это сделать безопасно (но обратите внимание, что вам нужен TextDecoderStream, где в этой демонстрации есть «TextDecoder»).

Обратите также внимание на использование WritableStream в этой демонстрации. Firefox еще не поддерживает его, насколько я знаю, но WritableStream предоставляет гораздо более простой синтаксис для использования фрагментов без необходимости явного цикла или рекурсии. Полифил веб-потоков можно найти здесь.

person lunchrhyme    schedule 15.10.2018
comment
Спасибо за исчерпывающий ответ. В конце концов, виновником были сами отладчики Chrome/Firefox!! Я изменил свой Console.Log на document.write для вывода отладки, закрыл отладчик, и первый пример кода отлично работал как с большими, так и с маленькими фрагментами. Мой код уже обрабатывал разделенные символы/JSON, хотя и не элегантно. Поэтому я переписал, как было предложено: pastebin.com/hWGtQZvA, но это было не так эффективно, как мой исходный код (на удивление). Таким образом, самый быстрый и на 100% нативный код получился как pastebin.com/Dr1CQVa2, который я сейчас уточню. Спасибо! - person QA Collective; 16.10.2018
comment
pipeThrough() еще не оптимизирован, поэтому read() по-прежнему дает наилучшую производительность, как вы обнаружили. - person lunchrhyme; 17.10.2018
comment
Есть ли способ определить, когда данные были разделены на разные куски? (В его случае вы предложили TextDecoderStream, но в моем случае здесь я получаю количество байтов, которые иногда разбиваются на разные куски...) - person rsc; 07.10.2020
comment
Чтобы определить, когда символ был разделен на несколько фрагментов, я думаю, вам нужно будет самостоятельно декодировать символы. Если ввод UTF-8, это не слишком сложно, но, конечно, будет очень медленно. - person lunchrhyme; 12.10.2020