mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
## Summary Fixes a crash in the Windows file watcher that occurred when the number of file system events exceeded the fixed `watch_events` buffer size (128). ## Problem The crash manifested as: ``` index out of bounds: index 128, len 128 ``` This happened when: 1. More than 128 file system events were generated in a single watch cycle 2. The code tried to access `this.watch_events[128]` on an array of length 128 (valid indices: 0-127) 3. Later, `std.sort.pdq()` would operate on an invalid array slice ## Solution Implemented a hybrid approach that preserves the original behavior while handling overflow gracefully: - **Fixed array for common case**: Uses the existing 128-element array when possible for optimal performance - **Dynamic allocation for overflow**: Switches to `ArrayList` only when needed - **Single-batch processing**: All events are still processed together in one batch, preserving event coalescing - **Graceful fallback**: Handles allocation failures with appropriate fallbacks ## Benefits - ✅ **Fixes the crash** while maintaining existing performance characteristics - ✅ **Preserves event coalescing** - events for the same file still get properly merged - ✅ **Single consolidated callback** instead of multiple partial updates - ✅ **Memory efficient** - no overhead for normal cases (≤128 events) - ✅ **Backward compatible** - no API changes ## Test Plan - [x] Compiles successfully with `bun run zig:check-windows` - [x] Preserves existing behavior for common case (≤128 events) - [x] Handles overflow case gracefully with dynamic allocation 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@bun.sh> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
import { spawn } from "bun";
|
|
import { describe, expect, test } from "bun:test";
|
|
import { bunEnv, bunExe, forEachLine, isASAN, isCI, tempDirWithFiles } from "harness";
|
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
describe("--hot with many directories", () => {
|
|
// TODO: fix watcher thread exit handling so the main thread waits for the
|
|
// watcher thread to exit. This causes a crash inside the libc exit() function
|
|
// that triggers in ASAN.
|
|
test.skipIf(isCI && isASAN)(
|
|
"handles 129 directories being updated simultaneously",
|
|
async () => {
|
|
// Create initial test structure
|
|
const tmpdir = tempDirWithFiles("hot-many-dirs", {
|
|
"entry.js": `console.log('Initial load');`,
|
|
});
|
|
|
|
// Generate 129 directories with files
|
|
const dirCount = 129;
|
|
const maxCount = 3;
|
|
for (let i = 0; i < dirCount; i++) {
|
|
const dirName = `dir-${i.toString().padStart(4, "0")}`;
|
|
const dirPath = join(tmpdir, dirName);
|
|
mkdirSync(dirPath, { recursive: true });
|
|
|
|
// Create an index.js in each directory
|
|
writeFileSync(join(dirPath, "index.js"), `export const value${i} = ${i};`);
|
|
}
|
|
|
|
// Create main index that imports all directories
|
|
const imports = Array.from({ length: dirCount }, (_, i) => {
|
|
const dirName = `dir-${i.toString().padStart(4, "0")}`;
|
|
return `import * as dir${i} from './${dirName}/index.js';`;
|
|
}).join("\n");
|
|
|
|
writeFileSync(
|
|
join(tmpdir, "entry.js"),
|
|
`
|
|
${imports}
|
|
console.log('Loaded', ${dirCount}, 'directories');
|
|
(globalThis.reloaded ??= 0);
|
|
if (globalThis.reloaded++ >= ${maxCount}) process.exit(0);
|
|
`,
|
|
);
|
|
|
|
// Start bun --hot
|
|
await using proc = spawn({
|
|
cmd: [bunExe(), "--hot", "entry.js"],
|
|
cwd: tmpdir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "inherit",
|
|
});
|
|
|
|
const stdout = proc.stdout;
|
|
|
|
const iter = forEachLine(stdout);
|
|
|
|
// Wait for initial load
|
|
let { value: line } = await iter.next();
|
|
expect(line).toContain(`Loaded ${dirCount} directories`);
|
|
|
|
// Trigger maxCount reload cycles
|
|
let reloadCount = 0;
|
|
|
|
for (let cycle = 0; cycle < maxCount; cycle++) {
|
|
// Update all files simultaneously
|
|
const timestamp = Date.now() + cycle;
|
|
const updatePromises = [];
|
|
|
|
for (let i = 0; i < dirCount; i++) {
|
|
const dirName = `dir-${i.toString().padStart(4, "0")}`;
|
|
const filePath = join(tmpdir, dirName, "index.js");
|
|
|
|
updatePromises.push(
|
|
Bun.write(filePath, `export const value${i} = ${i};\nexport const timestamp${i} = ${timestamp};`),
|
|
);
|
|
}
|
|
|
|
// Wait for all updates to complete
|
|
await Promise.all(updatePromises);
|
|
|
|
// Wait for reload message
|
|
({ value: line } = await iter.next());
|
|
expect(line).toContain(`Loaded ${dirCount} directories`);
|
|
reloadCount++;
|
|
}
|
|
|
|
// Verify we got maxCount successful reloads
|
|
expect(reloadCount).toBe(maxCount);
|
|
|
|
// Wait for the process to exit on its own after maxCount reloads
|
|
const exitCode = await proc.exited;
|
|
|
|
// Should exit with 0
|
|
expect(exitCode).toBe(0);
|
|
},
|
|
30000,
|
|
); // 30 second timeout
|
|
});
|