Skip to content

Commit 72100ee

Browse files
committed
perf: flush watched files
1 parent 2c76430 commit 72100ee

1 file changed

Lines changed: 111 additions & 52 deletions

File tree

packages/extension/src/watcher.ts

Lines changed: 111 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import * as vscode from 'vscode'
77
import { getConfig } from './config'
88
import { log } from './log'
99

10+
const DEBOUNCE_DELAY = 300
11+
1012
export class ExtensionWatcher extends vscode.Disposable {
1113
private watcherByFolder = new Map<vscode.WorkspaceFolder, vscode.FileSystemWatcher>()
1214
private apisByFolder = new WeakMap<vscode.WorkspaceFolder, VitestProcessAPI[]>()
15+
private debounceTimers = new Map<string, ReturnType<typeof setTimeout>>()
1316

1417
constructor(
1518
private readonly testTree: TestTree,
@@ -25,6 +28,8 @@ export class ExtensionWatcher extends vscode.Disposable {
2528
this.watcherByFolder.forEach((x) => x.dispose())
2629
this.watcherByFolder.clear()
2730
this.apisByFolder = new WeakMap()
31+
this.debounceTimers.forEach((timer) => clearTimeout(timer))
32+
this.debounceTimers.clear()
2833
}
2934

3035
watchTestFilesInWorkspace(api: VitestProcessAPI) {
@@ -44,82 +49,136 @@ export class ExtensionWatcher extends vscode.Disposable {
4449
const watcher = vscode.workspace.createFileSystemWatcher(pattern)
4550
this.watcherByFolder.set(folder, watcher)
4651

47-
watcher.onDidDelete(async (uri) => {
52+
const deleteQueue = new Map<string, vscode.Uri>()
53+
const changeQueue = new Map<string, vscode.Uri>()
54+
const createQueue = new Map<string, vscode.Uri>()
55+
56+
watcher.onDidDelete((uri) => {
4857
const path = normalize(uri.fsPath)
4958
if (this.isCommondIgnore(path)) {
5059
return
5160
}
52-
53-
log.verbose?.('[VSCODE] Item deleted:', this.relative(api, uri))
54-
55-
this.transformSchemaProvider.emitChange(uri)
56-
57-
// We don't know if it is a file or a folder
58-
this.testTree.removeFile(path)
59-
this.testTree.removeFolder(path)
61+
deleteQueue.set(path, uri)
62+
this.scheduleFlush(`delete:${folder.name}`, () => {
63+
const batch = new Map(deleteQueue)
64+
deleteQueue.clear()
65+
log.verbose?.(`[VSCODE] Flushing ${batch.size} deleted items`)
66+
for (const [path, uri] of batch) {
67+
this.transformSchemaProvider.emitChange(uri)
68+
// We don't know if it is a file or a folder
69+
this.testTree.removeFile(path)
70+
this.testTree.removeFolder(path)
71+
}
72+
})
6073
})
6174

62-
watcher.onDidChange(async (uri) => {
75+
watcher.onDidChange((uri) => {
6376
const path = normalize(uri.fsPath)
64-
const type = await this.getFsType(api, path, uri)
65-
if (type !== 'file') {
77+
if (this.isCommondIgnore(path)) {
6678
return
6779
}
80+
changeQueue.set(path, uri)
81+
this.scheduleFlush(`change:${folder.name}`, async () => {
82+
const batch = new Map(changeQueue)
83+
changeQueue.clear()
84+
log.verbose?.(`[VSCODE] Flushing ${batch.size} changed items`)
85+
const apis = this.apisByFolder.get(folder) || []
86+
for (const [path, uri] of batch) {
87+
const type = await this.getFsType(api, path, uri)
88+
if (type !== 'file') {
89+
continue
90+
}
91+
this.transformSchemaProvider.emitChange(uri)
92+
93+
apis.forEach((api) => {
94+
api.onFileChanged(path)
95+
const fileItems = this.testTree.getFileTestItems(path)
96+
97+
// Ignore changed to never opened files
98+
if (fileItems.every((item) => item.children.size === 0 && !item.error)) {
99+
return
100+
}
101+
102+
if (api.getPersistentProcessMeta() || api.isSpawningPersistentProcess) {
103+
return
104+
}
68105

69-
this.transformSchemaProvider.emitChange(uri)
70-
log.verbose?.('[VSCODE] File changed:', this.relative(api, uri))
71-
const apis = this.apisByFolder.get(folder) || []
72-
apis.forEach((api) => api.onFileChanged(path))
73-
apis.forEach((api) => {
74-
if (api.getPersistentProcessMeta() || api.isSpawningPersistentProcess) {
75-
return
106+
const metadata = api.getPotentialTestFileMetadata(path)
107+
metadata.forEach((meta) => {
108+
api.collectTests(meta.project, path)
109+
})
110+
})
76111
}
77-
const metadata = api.getPotentialTestFileMetadata(path)
78-
metadata.forEach((meta) => {
79-
api.collectTests(meta.project, path)
80-
})
81112
})
82113
})
83114

84-
watcher.onDidCreate(async (uri) => {
115+
watcher.onDidCreate((uri) => {
85116
const path = normalize(uri.fsPath)
86-
const type = await this.getFsType(api, path, uri)
87-
88-
if (!type) {
117+
if (this.isCommondIgnore(path)) {
89118
return
90119
}
120+
createQueue.set(path, uri)
121+
this.scheduleFlush(`create:${folder.name}`, async () => {
122+
const batch = new Map(createQueue)
123+
createQueue.clear()
124+
log.verbose?.(`[VSCODE] Flushing ${batch.size} created items`)
125+
126+
const apis = this.apisByFolder.get(folder) || []
127+
const roots = apis.flatMap((api) =>
128+
// TODO: resolve should be done on the worker side
129+
api.config.projects.map((p) => normalize(resolve(api.config.cwd, p.dir || p.root))),
130+
)
131+
const openedFiles = vscode.workspace.textDocuments.map((d) => normalize(d.uri.fsPath))
132+
133+
const allFiles: string[] = []
134+
for (const [path, uri] of batch) {
135+
const type = await this.getFsType(api, path, uri)
136+
if (!type) {
137+
continue
138+
}
139+
if (type === 'file') {
140+
allFiles.push(path)
141+
} else {
142+
allFiles.push(...(await this.readFilesRecursively(uri, roots)))
143+
}
144+
}
91145

92-
log.verbose?.('[VSCODE]', 'New', type, 'created:', this.relative(api, uri))
93-
94-
const apis = this.apisByFolder.get(folder) || []
95-
const roots = apis.flatMap((api) =>
96-
// TODO: resolve should be done on the worker side
97-
api.config.projects.map((p) => normalize(resolve(api.config.cwd, p.dir || p.root))),
98-
)
99-
const files = type === 'file' ? [path] : await this.readFilesRecursively(uri, roots)
100-
const openedFiles = vscode.workspace.textDocuments.map((d) => normalize(d.uri.fsPath))
101-
102-
files.forEach((file) => {
103-
apis.forEach((api) => {
104-
const metadata = api.getPotentialTestFileMetadata(file)
105-
metadata.forEach((meta) => {
106-
this.testTree.getOrCreateFileTestItem(api, meta, file)
107-
108-
// If file is open and not a continuous run,
109-
// Collect its tests immidetly, otherwise ignore
110-
if (
111-
openedFiles.includes(file) &&
112-
!api.getPersistentProcessMeta() &&
113-
!api.isSpawningPersistentProcess
114-
) {
115-
api.collectTests(meta.project, file)
116-
}
146+
allFiles.forEach((file) => {
147+
apis.forEach((api) => {
148+
const metadata = api.getPotentialTestFileMetadata(file)
149+
metadata.forEach((meta) => {
150+
this.testTree.getOrCreateFileTestItem(api, meta, file)
151+
152+
// If file is open and not a continuous run,
153+
// Collect its tests immidetly, otherwise ignore
154+
if (
155+
openedFiles.includes(file) &&
156+
!api.getPersistentProcessMeta() &&
157+
!api.isSpawningPersistentProcess
158+
) {
159+
api.collectTests(meta.project, file)
160+
}
161+
})
117162
})
118163
})
119164
})
120165
})
121166
}
122167

168+
private scheduleFlush(key: string, flush: () => void) {
169+
const existing = this.debounceTimers.get(key)
170+
if (existing) {
171+
clearTimeout(existing)
172+
}
173+
this.debounceTimers.set(
174+
key,
175+
setTimeout(() => {
176+
this.debounceTimers.delete(key)
177+
flush()
178+
}, DEBOUNCE_DELAY),
179+
)
180+
}
181+
123182
private relative(api: VitestProcessAPI, uri: vscode.Uri) {
124183
return relative(api.workspaceFolder.uri.fsPath, uri.fsPath)
125184
}

0 commit comments

Comments
 (0)