Skip to content

Heap-snapshot worker is leaked when loading a snapshot fails (missing/invalid file) #2448

Description

@ch-bas

Description of the bug

HeapSnapshotManager.#loadSnapshot creates a HeapSnapshotWorkerProxy (a live worker) before it reads the snapshot file, but the manager only ever disposes workers that are tracked in the #snapshots map:

// src/HeapSnapshotManager.ts
async #loadSnapshot(absolutePath, uid) {
  const workerProxy = new ...HeapSnapshotWorkerProxy(...);   // spawns a worker
  const loaderProxy = workerProxy.createLoader(uid, ...);
  const fileStream = fsSync.createReadStream(absolutePath, {...});
  for await (const chunk of fileStream) {   // throws ENOENT for a missing/unreadable file
    await loaderProxy.write(chunk);
  }
  await loaderProxy.close();
  const snapshot = await snapshotPromise;
  return {snapshot, worker: workerProxy};
}

getSnapshot has no try/catch and only calls #snapshots.set(...) on success. So when a load fails, the worker is never added to #snapshots, and therefore neither dispose(filePath) nor disposeAll() (added in #2428 for context teardown) can ever reach it. The worker leaks for the life of the process — one worker per failed load.

verifyFilesSchema: ['filePath'] on the read tools only checks the path is inside the workspace roots; it does not verify the file exists or is a valid snapshot.

Reproduction

  1. Call any of get_heapsnapshot_summary / get_heapsnapshot_details / get_heapsnapshot_class_nodes / get_heapsnapshot_retainers / get_heapsnapshot_retaining_paths / get_heapsnapshot_edges with a filePath that is inside a workspace root but does not exist (or is a truncated / non-heapsnapshot file).
  2. The call fails, and the worker created for it is never disposed.
  3. Repeat → workers accumulate.

A direct unit test that fails on main:

const disposeSpy = sinon.spy(
  DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy.prototype,
  'dispose',
);
const manager = new HeapSnapshotManager();
await assert.rejects(manager.getSnapshot('/nonexistent/does-not-exist.heapsnapshot'));
sinon.assert.calledOnce(disposeSpy); // 0 calls on main

Expectation

A failed snapshot load disposes its worker rather than leaking it.

Fix: wrap the load body in try/catch and workerProxy.dispose() before rethrowing. I have this plus the regression test passing locally and can open a PR.

Chrome DevTools MCP version

main @ 574c320 (1.6.0)

Chrome version

n/a (independent of Chrome version)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions