node.js - Att använda 'for await x of y' med en AsyncIterator orsakar minnesläcka
När jag använder AsyncIterator har jag en betydande minnesläcka när det används i for-x-of-y
Jag behöver detta när jag skrapar en HTML-sida som innehåller information om nästa HTML-sida som ska skrapas:
- Skrapa data
- Utvärdera data
- Skrapa nästa data
Den asynkrona delen behövs eftersom axios används för att hämta HTML
Här är ett exempel, som gör det möjligt att se minnet öka från ~4MB till ~25MB i slutet av skriptet. Minnet frigörs inte förrän programmet avslutas.
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()
Det ser ut som att data för for-await-x-of-y hänger kvar i minnet. Anropsstacken blir också stor.
I reproduktionen kunde problemet fortfarande hanteras. Men för min faktiska kod stannar en hel HTML-sida i minnet, vilket är ~250 kb vid varje anrop.
I denna skärmdump kan du se heapminnet vid den första iterationen jämfört med heapminnet efter den sista iterationen
Kan ännu inte posta skärmdumpar direkt
Det förväntade arbetsflödet skulle vara följande:
- Hämta data
- Bearbeta data
- Extrahera information för nästa "Hämta data"
- Frigör allt minne från den senaste "Hämta data"
- Använd den extraherade informationen för att starta om loopen med nya data som erhållits.
Jag är osäker på om en AsyncIterator är rätt val här för att arkivera det som behövs.
All hjälp/tips skulle uppskattas!
Svar #1
Kort sagt
När man använder en AsyncIterator ökar minnet drastiskt. Det sjunker när iterationen är klar.
Variabeln x i `for await (x of y) frigörs inte förrän iterationen är klar. Även varje Promise som väntas inne i for-loopen frigörs inte.
Jag kom fram till slutsatsen att Garbage Collector inte kan fånga innehållet i Iteration, eftersom de Promises som genereras av AsyncIterator bara kommer att lösas helt när Iteration är klar. Jag tror att detta kan vara en bugg.
Arbetsrundning Repro
Som en lösning för att frigöra innehållet i Parsern kapslar vi in Resultatet i en lättviktsbehållare. Vi frigör sedan innehållet, så att endast behållaren själv återstår i minnet. data-objektet kan inte frigöras även om du använder samma teknik för att kapsla in det - så verkar det vara fallet åtminstone vid felsökning.
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())
Tillfällig lösning i verkligt sammanhang
Vad som inte visas i Repro-lösningen är att vi också försöker frigöra Resultatet av själva iterationen. Detta verkar dock inte ha någon effekt(?).
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();
}
}
Se: https://github.com/demokratie-live/scapacra/blob/master/src/Scraper.ts För att testa med en faktisk applikation: https://github.com/demokratie-live/scapacra-bt (yarn dev ConferenceWeekDetail)
Referenser
- Github NodeJs: https://github.com/nodejs/node/issues/30298
- Github DEMOKRATI: https://github.com/demokratie-live/democracy-client/issues/926
Slutsats
Vi hittade en genomförbar lösning för oss. Därför stänger jag detta ärende. Uppföljningen riktas mot Node.js-repot för att åtgärda denna potentiella bugg
https://github.com/nodejs/node/issues/30298
