Skip to content

@swc/cli: swcDir() leaks a Piscina worker pool per invocation when used as a library #126

Description

initialCompilation() in packages/cli/src/swc/dir.ts constructs a new Piscina({...}) and never destroys it. Harmless for one-shot CLI runs (the process exits), but swcDir() is also consumed as a library by long-lived processes — @nestjs/cli's swc builder re-invokes it on every file-add event in watch mode.

Each leaked pool permanently pins minThreads (= cores / 2) eagerly-spawned idle workers — piscina's default idleTimeout never reaps below the minimum. Observed in a week-long nest start --watch -b swc session on macOS:

  1. ~1,700 idle WorkerThreads, ~15 GB physical footprint (vmmap: dominated by tens of thousands of idle V8 isolate regions), one kqueue + one directory fd each (lsof);
  2. inspector NodeWorker enumeration showed every worker parked at piscina/dist/worker.js;
  3. thread count grows monotonically with src file-add events (renames and git checkouts count — atomic writes surface as add).

Repro without nest:

const swcDir = require("@swc/cli/lib/swc/dir").default;

(async () => {
  for (let i = 0; i < 10; i++) {
    await swcDir({
      cliOptions: {
        filenames: ["src"], outDir: "dist", sync: false, watch: false,
        extensions: [".ts"], copyFiles: false, includeDotfiles: false,
        stripLeadingPaths: false, quiet: true,
      },
      swcOptions: {},
    });
    // thread count grows by ~cores/2 per iteration:
    //   macOS: ps -M <pid> | wc -l      Linux: ls /proc/<pid>/task | wc -l
  }
})();

The watch path compiles via dirWorker directly and never uses this pool, so destroying it after the initial batch (e.g. .finally(() => workers.destroy())) is behavior-neutral.


This issue was investigated and drafted by Claude; I'm following up and available for a PR if wanted.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions