Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Idiomatic Streams usage with Promises causes memory leaks #5391

Open
scheibo opened this issue Mar 31, 2019 · 10 comments
Open

Idiomatic Streams usage with Promises causes memory leaks #5391

scheibo opened this issue Mar 31, 2019 · 10 comments

Comments

@scheibo
Copy link
Contributor

scheibo commented Mar 31, 2019

From #5370 (comment)

[20:29:07] @Slayer95: pre, fixed the memory leak by doing await new Promise(resolve => {setImmediate(resolve)}) every N games run
[20:30:14] @Slayer95: as it turns out, there is not a single setImmediate in the BattleStream implementation, so it's in fact a sync stream
[20:30:50] @Slayer95: now, exactly how that impacts the GC logic isn't very clear
[20:30:56] @pre: i think thats a `BattleStream` or `Steams` issue then?
[20:31:01] @pre: but thanks for figuring that out
[20:31:15] @Slayer95: there are very few articles discussing memory leaks from looping async
[20:31:19] @pre: i was literally just commenting on your callback implementation from yesterday
[20:31:42] @Slayer95: yes, a fix in BattleStream / Streams should be feasible
@Zarel
Copy link
Member

Zarel commented Mar 31, 2019

Streams are supposed to be either sync or async depending on which you prefer.

Also, in theory the async keyword compiles to the equivalent of a setImmediate.

@Zarel
Copy link
Member

Zarel commented Mar 31, 2019

Also: BattleStreams are designed to be sync.

@scheibo
Copy link
Contributor Author

scheibo commented Mar 31, 2019

So this should be closed as WAI? And users should just use setImmediate as needed? Perhaps we should add that guidance to STREAMS.md?

@Zarel
Copy link
Member

Zarel commented Mar 31, 2019

Well, if it leads to a memory leak under normal usage, clearly something needs to be fixed. We should just figure out why.

@Zarel
Copy link
Member

Zarel commented Mar 31, 2019

It's unclear to me why using a sync stream would cause a memory leak. Do you have details?

@Slayer95
Copy link
Contributor

Also, in theory the async keyword compiles to the equivalent of a setImmediate.

Not really.

await x();
y();

is mostly equivalent to

x(() => process.nextTick(y));

But ofc, nextTick() doesn't schedule y for the next tick, but for the current tick.

@Slayer95
Copy link
Contributor

Slayer95 commented Mar 31, 2019

When running this excerpt from #5370

		while ((format = this.getNextFormat())) {
			const seed = this.prng.seed;
			lastFormat = format;
			await new Runner(Object.assign({format}, this.options)).run().catch(err => {
				failures++;
				console.error(
					`Run \`node dev-tools/harness 1 --format=${format} --seed=${seed.join()}\` ` +
					`to debug (optionally with \`--output\` and/or \`--input\` for more info):\n`, err);
			});
		}

The heap accumulates Promise and TickObject instances, which aren't collected even when calling gc() (with --expose-gc flag). Those magically go away by await-ing for a setImmediate() call.

Now!!! Last night I was speculating that this may be an engine regression, since V8 has put some work in optimizing promise ticks in recent versions.... And... That's indeed the case !

https://gist.github.com/Slayer95/6456a5d29ca17935d62d301f4a76d389

Test case leaks in Node v10.15.3, but it doesn't in v8.15.1 !

PS. Reproduced in v11.13.0, v12.0.0-nightly20190331bb98f27181, and v12.0.0-v8-canary201903313c649ecee6

@Zarel
Copy link
Member

Zarel commented Mar 31, 2019

Looks like you should report a V8 bug, and I should file this away as "not my fault". ;)

Feel free to work around with the setImmediate await.

@Slayer95
Copy link
Contributor

Slayer95 commented Mar 31, 2019

Narrowed down to this code. Only the deepCloneSync() variant leaks memory. The alternative deepClone.* functions don't.

However, I think the harness code isn't using Promise.race() right... As a matter of fact, changing runGame() to this doesn't leak at all.

async function runGame(logOutput) {
	let chunk;
	while ((chunk = await MY_STREAM.read()))) {
		if (logOutput) console.log(chunk);
	}
}

@Slayer95
Copy link
Contributor

Slayer95 commented Apr 1, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants