Repository navigation
Description
Version
v26.0.0 (also v24.22.1)
Platform
Darwin [Redacted] 25.6.0 Darwin Kernel Version 25.6.0: Tue Aug 18 17:51:47 PDT 2026; root:xnu-12377.161.15.700.19~2/RELEASE_ARM64_T6050 arm64
Subsystem
fs
What steps will reproduce the bug?
On MacOS, run this script using node. It closes and reopens N directory watchers and prints the mem growth as reported by vmap:
import { watch, mkdtempSync, mkdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
const footprintKB = () => {
const out = execFileSync('vmmap', ['--summary', String(process.pid)]).
toString();
const [, n, unit] = out.match(/Physical footprint:\s+([\d.]+)([KMG])/);
return Number(n) * { K: 1, M: 1024, G: 1024 * 1024 }[unit];
};
const tick = () => new Promise((r) => setImmediate(r));
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
for (const n of [10, 100, 400, 1000]) {
const base = mkdtempSync(join(tmpdir(), 'fsw-'));
const dirs = Array.from({ length: n }, (_, i) => join(base, `d${i}`));
for (const d of dirs) mkdirSync(d);
const watchers = dirs.map((d) => watch(d, () => {}));
await sleep(500);
const before = footprintKB();
for (let i = 0; i < 1000; i++) {
watchers[0].close();
watchers[0] = watch(dirs[0], () => {});
if (i % 20 === 0) await tick();
}
await sleep(500);
const growth = footprintKB() - before;
console.log(`${n} watchers open: +${(growth / 1024).toFixed(1)} MB after
1000 reopens (${(growth / 1000).toFixed(1)} KB per reopen)`);
for (const w of watchers) w.close();
await sleep(500);
}
process.exit(0);How often does it reproduce? Is there a required condition?
Every run on MacOS
What is the expected behavior? Why is that the expected behavior?
I would expect physical memory usage to stay about flat.
What do you see instead?
10 watchers open: +4.4 MB after 1000 reopens (4.5 KB per reopen)
100 watchers open: +11.8 MB after 1000 reopens (12.1 KB per reopen)
400 watchers open: +48.8 MB after 1000 reopens (50.0 KB per reopen)
1000 watchers open: +114.8 MB after 1000 reopens (117.6 KB per reopen)
Additional information
This is fixed in libuv by libuv/libuv#5283 ("darwin: release fsevents path allocations"). uv__fsevents_reschedule rebuilds the shared FSEventStream from every watched path each time a watcher starts or stops, and did not release the path array and strings after a successful rebuild.
That commit is not in a libuv release yet. It merged after v1.53.0, so #66282 (update to libuv 1.53.0) does not include it. Could it be cherry-picked into deps/uv , as was done for aabb7651de in #62561, and backported to v24.x?
Downstream report: oxc-project/oxc-node#906
I used an AI coding assistant to help find the cause and draft this report. I ran the reproduction above myself on both versions listed and checked the libuv change against src/unix/fsevents.c .
Activity