Skip to content

Memory leak when awaiting in inspector Debugger.pause callback #51397

@timfish

Description

@timfish

Version

20.10.0

Platform

All

Subsystem

inspector

What steps will reproduce the bug?

The following code leaks memory at about 10MB per second on my machine:

import { Session } from "node:inspector/promises";

const session = new Session();
session.connect();

session.on("Debugger.paused", async (event) => {
  for (const frame of event.params.callFrames) {
    const localScope = frame.scopeChain.find((scope) => scope.type === "local");

    if (localScope?.object?.objectId) {
      const props = await session.post("Runtime.getProperties", {
        objectId: localScope.object.objectId,
        ownProperties: true,
      });
    }
  }
});

await session.post("Debugger.enable");
await session.post("Debugger.setPauseOnExceptions", { state: "all" });

setInterval(() => {
  console.log(`${Math.floor(process.memoryUsage().rss / 1024 / 1024)} MB`);
}, 1000);

setInterval(() => {
  try {
    throw new Error("Hello");
  } catch {}
}, 1);

How often does it reproduce? Is there a required condition?

Every time

What is the expected behavior? Why is that the expected behavior?

No memory leaked

What do you see instead?

I see ~10MB leaked every second which is around 10kB per debugger pause event.

Additional information

I found that that the following code:

import { Session } from "node:inspector/promises";

const session = new Session();
session.connect();

session.on("Debugger.resumed", () => {
  console.log("Debugger resumed");
});

session.on("Debugger.paused", async (event) => {
  console.log("Debugger paused start");

  for (const frame of event.params.callFrames) {
    const localScope = frame.scopeChain.find((scope) => scope.type === "local");

    if (localScope?.object?.objectId) {
      console.log("Runtime.getProperties");
      const props = await session.post("Runtime.getProperties", {
        objectId: localScope.object.objectId,
      });
      console.log("Runtime.getProperties returned");
    }
  }

  console.log("Debugger.paused end");
});

await session.post("Debugger.enable");
await session.post("Debugger.setPauseOnExceptions", { state: "all" });

setTimeout(() => {
  try {
    throw new Error("Hello");
  } catch {}
}, 1000);

Outputs:

Debugger paused start
Runtime.getProperties
Debugger resumed
Runtime.getProperties returned
Runtime.getProperties
Runtime.getProperties returned
Runtime.getProperties
Runtime.getProperties returned
Debugger.paused end

The debugger does not wait for Debugger.resume. Instead it resumes automatically as soon as we call await session.post and I guess calling Runtime.getProperties after the resume causes something to be leaked?

Metadata

Metadata

Assignees

No one assigned

    Labels

    inspectorIssues and PRs related to the V8 inspector protocol

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions