mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
perf: bound WSL watcher polling (#4191)
This commit is contained in:
@@ -143,6 +143,40 @@ describe('createWslWatcher', () => {
|
||||
await root.subscription.unsubscribe()
|
||||
})
|
||||
|
||||
it('limits concurrent child directory reads during WSL snapshots', async () => {
|
||||
const scheduleBatchFlush = vi.fn()
|
||||
const childDirs = Array.from({ length: 40 }, (_, index) => dirent(`dir-${index}`, 'dir'))
|
||||
let activeChildReads = 0
|
||||
let maxActiveChildReads = 0
|
||||
|
||||
readdirMock.mockImplementation((dirPath: string) => {
|
||||
if (dirPath === rootPath) {
|
||||
return Promise.resolve(childDirs)
|
||||
}
|
||||
activeChildReads += 1
|
||||
maxActiveChildReads = Math.max(maxActiveChildReads, activeChildReads)
|
||||
return new Promise<ReturnType<typeof dirent>[]>((resolve) => {
|
||||
setTimeout(() => {
|
||||
activeChildReads -= 1
|
||||
resolve([])
|
||||
}, 1)
|
||||
})
|
||||
})
|
||||
|
||||
const rootPromise = createWslWatcher(rootKey, rootPath, deps(scheduleBatchFlush))
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
|
||||
expect(maxActiveChildReads).toBeLessThanOrEqual(8)
|
||||
for (let i = 0; i < childDirs.length; i += 1) {
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
}
|
||||
|
||||
const root = await rootPromise
|
||||
expect(maxActiveChildReads).toBeLessThanOrEqual(8)
|
||||
await root.subscription.unsubscribe()
|
||||
})
|
||||
|
||||
it('marks a large WSL poll event batch for overflow without retaining every event', async () => {
|
||||
const scheduleBatchFlush = vi.fn()
|
||||
const initialEntries = Array.from({ length: 200_000 }, (_, index) => dirent(`file-${index}.ts`))
|
||||
|
||||
@@ -14,7 +14,7 @@ import { readdir } from 'fs/promises'
|
||||
import * as path from 'path'
|
||||
import type { WebContents } from 'electron'
|
||||
import type { Event as WatcherEvent } from '@parcel/watcher'
|
||||
import { queueWatcherEvents } from './filesystem-watcher-event-batch'
|
||||
import { MAX_BATCHED_WATCHER_EVENTS, queueWatcherEvents } from './filesystem-watcher-event-batch'
|
||||
|
||||
export type WatcherSubscription = {
|
||||
unsubscribe(): Promise<void>
|
||||
@@ -40,6 +40,8 @@ export type WslWatcherDeps = {
|
||||
}
|
||||
|
||||
const POLL_INTERVAL_MS = 2000
|
||||
const SNAPSHOT_CHILD_READ_CONCURRENCY = 8
|
||||
const DIFF_EVENT_OVERFLOW_LIMIT = MAX_BATCHED_WATCHER_EVENTS + 1
|
||||
|
||||
type DirSnapshot = Map<string, Set<string>>
|
||||
|
||||
@@ -56,6 +58,23 @@ function shouldIgnore(name: string, ignoreDirs: string[]): boolean {
|
||||
return ignoreDirs.includes(name)
|
||||
}
|
||||
|
||||
async function forEachWithConcurrency<T>(
|
||||
items: readonly T[],
|
||||
limit: number,
|
||||
worker: (item: T) => Promise<void>
|
||||
): Promise<void> {
|
||||
let nextIndex = 0
|
||||
const workerCount = Math.min(limit, items.length)
|
||||
await Promise.all(
|
||||
Array.from({ length: workerCount }, async () => {
|
||||
while (nextIndex < items.length) {
|
||||
const item = items[nextIndex++]
|
||||
await worker(item)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a snapshot of the root directory and one level of subdirectories.
|
||||
* Returns a map of dirPath → set of entry names.
|
||||
@@ -70,22 +89,24 @@ async function takeSnapshot(rootPath: string, ignoreDirs: string[]): Promise<Dir
|
||||
// Why: poll one level of subdirectories so changes inside immediate
|
||||
// children are detected, but use Dirent metadata to avoid probing every
|
||||
// root-level file with a failing readdir on each WSL poll.
|
||||
await Promise.all(
|
||||
filtered
|
||||
.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
|
||||
.map(async (entry) => {
|
||||
const childPath = path.join(rootPath, entry.name)
|
||||
const childEntries = await readDirEntriesSafe(childPath)
|
||||
const childFiltered = childEntries
|
||||
.filter((childEntry) => !shouldIgnore(childEntry.name, ignoreDirs))
|
||||
.map((childEntry) => childEntry.name)
|
||||
snapshot.set(childPath, new Set(childFiltered))
|
||||
})
|
||||
)
|
||||
const childDirs = filtered.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
|
||||
await forEachWithConcurrency(childDirs, SNAPSHOT_CHILD_READ_CONCURRENCY, async (entry) => {
|
||||
const childPath = path.join(rootPath, entry.name)
|
||||
const childEntries = await readDirEntriesSafe(childPath)
|
||||
const childFiltered = childEntries
|
||||
.filter((childEntry) => !shouldIgnore(childEntry.name, ignoreDirs))
|
||||
.map((childEntry) => childEntry.name)
|
||||
snapshot.set(childPath, new Set(childFiltered))
|
||||
})
|
||||
|
||||
return snapshot
|
||||
}
|
||||
|
||||
function appendDiffEvent(events: WatcherEvent[], event: WatcherEvent): boolean {
|
||||
events.push(event)
|
||||
return events.length >= DIFF_EVENT_OVERFLOW_LIMIT
|
||||
}
|
||||
|
||||
/**
|
||||
* Diff two snapshots and return synthetic watcher events.
|
||||
*/
|
||||
@@ -97,7 +118,14 @@ function diffSnapshots(prev: DirSnapshot, next: DirSnapshot): WatcherEvent[] {
|
||||
if (!prevEntries) {
|
||||
// New directory appeared — emit create for all entries
|
||||
for (const name of nextEntries) {
|
||||
events.push({ type: 'create', path: path.join(dirPath, name) } as WatcherEvent)
|
||||
if (
|
||||
appendDiffEvent(events, {
|
||||
type: 'create',
|
||||
path: path.join(dirPath, name)
|
||||
} as WatcherEvent)
|
||||
) {
|
||||
return events
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -105,14 +133,28 @@ function diffSnapshots(prev: DirSnapshot, next: DirSnapshot): WatcherEvent[] {
|
||||
// Check for new entries (create)
|
||||
for (const name of nextEntries) {
|
||||
if (!prevEntries.has(name)) {
|
||||
events.push({ type: 'create', path: path.join(dirPath, name) } as WatcherEvent)
|
||||
if (
|
||||
appendDiffEvent(events, {
|
||||
type: 'create',
|
||||
path: path.join(dirPath, name)
|
||||
} as WatcherEvent)
|
||||
) {
|
||||
return events
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for removed entries (delete)
|
||||
for (const name of prevEntries) {
|
||||
if (!nextEntries.has(name)) {
|
||||
events.push({ type: 'delete', path: path.join(dirPath, name) } as WatcherEvent)
|
||||
if (
|
||||
appendDiffEvent(events, {
|
||||
type: 'delete',
|
||||
path: path.join(dirPath, name)
|
||||
} as WatcherEvent)
|
||||
) {
|
||||
return events
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,7 +162,9 @@ function diffSnapshots(prev: DirSnapshot, next: DirSnapshot): WatcherEvent[] {
|
||||
// Check for directories that disappeared entirely
|
||||
for (const [dirPath] of prev) {
|
||||
if (!next.has(dirPath)) {
|
||||
events.push({ type: 'delete', path: dirPath } as WatcherEvent)
|
||||
if (appendDiffEvent(events, { type: 'delete', path: dirPath } as WatcherEvent)) {
|
||||
return events
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user