Поскольку ожидание x of y использование AsyncIterator вызывает утечку памяти - PullRequest
1 голос
/ 18 октября 2019

При использовании AsyncIterator у меня возникает незначительная утечка памяти при использовании в for-x-of-y

Мне это нужно при очистке HTML-страницы, которая включает в себя информацию о следующей HTML-странице, которая будет очищена:

  1. Данные скрапа
  2. Оценка данных
  3. Очистка следующих данных

Асинхронная часть необходима, поскольку Axios используется для полученияHTML

Вот репродукция, позволяющая увидеть, как объем памяти увеличивается от ~ 4 МБ до ~ 25 МБ в конце скрипта. Память не освобождается до завершения программы.

const scraper = async ():Promise<void> => {
    let browser = new BrowserTest();
    let parser = new ParserTest();

    for await (const data of browser){
        console.log(await parser.parse(data))
    }
}

class BrowserTest {
    private i: number = 0;

    public async next(): Promise<IteratorResult<string>> {
        this.i += 1;
        return {
            done: this.i > 1000,
            value: 'peter '.repeat(this.i)
        }
    }

    [Symbol.asyncIterator](): AsyncIterator<string> {
        return this;
    }
}

class ParserTest {
    public async parse(data: string): Promise<string[]> {
        return data.split(' ');
    }
}

scraper()

Похоже, что data for-await-x-of-y болтается в памяти. Коллстак также становится огромным.

В репродукции проблема все еще может быть решена. Но для моего реального кода вся HTML-страница остается в памяти, которая составляет ~ 250 Кбайт на каждый вызов.

На этом снимке экрана вы можете увидеть кучную память на первой итерации по сравнению с кучей памяти после последней итерации

Невозможно опубликовать встроенные снимки экрана

Ожидаемый рабочий процесс будет следующим:

  • Получение данных
  • Данные процесса
  • Извлечение информации для следующего «получения данных»
  • Освобождение всей памяти из последних «получения данных»
  • Использование извлеченной информации для перезапуска цикла с новыми полученными данными.

Я не уверен, что AsyncIterator является правильным выбором для архивирования того, что необходимо.

Любая помощь / подсказка будет оценена!

1 Ответ

1 голос
/ 06 ноября 2019

Короче

При использовании AsyncIterator память резко увеличивается. Он сбрасывается после завершения итерации.

x в `для await (x of y) не освобождается, пока итерация не будет завершена. Также каждое Обещание, ожидаемое внутри цикла for, не освобождается.

Я пришел к выводу, что Сборщик мусора не может перехватить содержимое Итерации, поскольку Обещания, сгенерированные AsyncIterator, будут полностью разрешены только после того, какИтерация завершена. Я думаю, что это может быть ошибка.

Обходное решение Repro

В качестве обходного пути для освобождения содержимого анализатора мы инкапсулируем Result в легкий контейнер. Затем мы освобождаем содержимое, поэтому в памяти остается только сам контейнер. data Объект не может быть освобожден, даже если вы используете ту же технику для его инкапсуляции - так что, похоже, это имеет место при отладке по крайней мере.

const scraper = async ():Promise<void> => {
    let browser = new BrowserTest();

    for await (const data of browser){
        let parser = new ParserTest();
        let result = await parser.parse(data);
        console.log(result);

        /**
         * This avoids memory leaks, due to a garbage collector bug
         * of async iterators in js
         */
        result.free();
    }
}

class BrowserTest {
    private i: number = 0;
    private value: string = "";

    public async next(): Promise<IteratorResult<string>> {
        this.i += 1;
        this.value = 'peter '.repeat(this.i);
        return {
            done: this.i > 1000,
            value: this.value
        }
    }

    public [Symbol.asyncIterator](): AsyncIterator<string> {
        return this;
    }
}

/**
 * Result class for wrapping the result of the parser.
 */
class Result {
    private result: string[] = [];

    constructor(result: string[]){
        this.setResult(result);
    }

    public setResult(result: string[]) {
        this.result = result;
    }

    public getResult(): string[] {
        return this.result;
    }

    public free(): void {
        delete this.result;
    }
}

class ParserTest {
    public async parse(data: string): Promise<Result>{
        let result = data.split(' ');
        return new Result(result);
    }
}

scraper())

Обходной путь в реальном контексте

В Repro-Solution не показано, что мы также пытаемся освободить сам результат итерации. Кажется, что это не имеет никакого эффекта, хотя (?).

public static async scrape<D,M>(scraper: IScraper<D,M>, callback: (data: DataPackage<Object,Object> | null) => Promise<void>) {
        let browser = scraper.getBrowser();
        let parser = scraper.getParser();

        for await (const parserFragment of browser) {
            const fragment = await parserFragment;
            const json = await parser.parse(fragment);
            await callback(json);
            json.free();
            fragment.free();
        }
    }

См .: https://github.com/demokratie-live/scapacra/blob/master/src/Scraper.ts Чтобы проверить с реальным приложением: https://github.com/demokratie-live/scapacra-bt (yarn dev ConferenceWeekDetail)

Ссылки

Вывод

Мы нашлиВозможное решение для нас. Поэтому я закрываю этот выпуск. Отслеживание направлено на репо Node.js, чтобы исправить эту потенциальную ошибку

https://github.com/nodejs/node/issues/30298

...