Skip to content

Commit

Permalink
perf: use vscode watcher instead of chokidar (#462)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored Sep 4, 2024
1 parent 6702c6a commit a815391
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 29 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ These options are resolved relative to the [workspace file](https://code.visuals

### Other Options

- `vitest.filesWatcherInclude`: Glob pattern for the watcher that triggers a test rerun or collects changes. Default: `**/*`
- `vitest.vitestPackagePath`: The path to a `package.json` file of a Vitest executable (it's usually inside `node_modules`) in case the extension cannot find it. It will be used to resolve Vitest API paths. This should be used as a last resort fix.
- `vitest.nodeEnv`: Environment passed to the runner process in addition to
`process.env`
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@
"configuration": {
"title": "Vitest",
"properties": {
"vitest.filesWatcherInclude": {
"markdownDescription": "The glob pattern to watch for file changes.",
"type": "string",
"default": "**/*",
"scope": "resource"
},
"vitest.vitestPackagePath": {
"markdownDescription": "The path to the `package.json` file of Vitest executable (it's usually inside `node_modules`) in case the extension cannot find it.",
"type": "string",
Expand Down
68 changes: 40 additions & 28 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,38 +143,28 @@ export class VitestFolderAPI extends VitestReporter {
return this.meta.rpc.getFiles()
}

private testsQueue = new Set<string>()
private collectPromise: Promise<void> | null = null
private collectTimer: NodeJS.Timeout | null = null

async collectTests(projectName: string, testFile: string) {
this.testsQueue.add(`${projectName}\0${normalize(testFile)}`)

if (this.collectTimer) {
clearTimeout(this.collectTimer)
}

await this.collectPromise
onFileCreated = createQueuedHandler((files: string[]) => {
return this.meta.rpc.onFilesCreated(files)
})

if (this.collectTimer) {
clearTimeout(this.collectTimer)
}
onFileChanged = createQueuedHandler((files: string[]) => {
return this.meta.rpc.onFilesChanged(files)
})

this.collectTimer = setTimeout(() => {
const tests = Array.from(this.testsQueue).map((spec) => {
const [projectName, filepath] = spec.split('\0', 2)
return [projectName, filepath] as [string, string]
})
const root = this.workspaceFolder.uri.fsPath
this.testsQueue.clear()
log.info('[API]', `Collecting tests: ${tests.map(t => relative(root, t[1])).join(', ')}`)
this.collectPromise = this.meta.rpc.collectTests(tests).finally(() => {
this.collectPromise = null
})
}, 50)
await this.collectPromise
async collectTests(projectName: string, testFile: string) {
return this._collectTests(`${projectName}\0${normalize(testFile)}`)
}

private _collectTests = createQueuedHandler((testsQueue: string[]) => {
const tests = Array.from(testsQueue).map((spec) => {
const [projectName, filepath] = spec.split('\0', 2)
return [projectName, filepath] as [string, string]
})
const root = this.workspaceFolder.uri.fsPath
log.info('[API]', `Collecting tests: ${tests.map(t => relative(root, t[1])).join(', ')}`)
return this.meta.rpc.collectTests(tests)
})

async dispose() {
this.handlers.clearListeners()
delete require.cache[this.meta.pkg.vitestPackageJsonPath]
Expand Down Expand Up @@ -224,6 +214,28 @@ export class VitestFolderAPI extends VitestReporter {
}
}

function createQueuedHandler<T>(resolver: (value: T[]) => Promise<void>) {
const cached = new Set<T>()
let promise: Promise<void> | null = null
let timer: NodeJS.Timeout | null = null
return (value: T) => {
cached.add(value)
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
if (promise) {
return
}
const values = Array.from(cached)
cached.clear()
promise = resolver(values).finally(() => {
promise = null
})
}, 50)
}
}

export async function resolveVitestAPI(packages: VitestPackage[]) {
const promises = packages.map(async (pkg) => {
const vitest = await createVitestProcess(pkg)
Expand Down
3 changes: 3 additions & 0 deletions src/api/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export interface VitestMethods {
disableCoverage: () => void
waitForCoverageReport: () => Promise<string | null>
close: () => void

onFilesCreated: (files: string[]) => void
onFilesChanged: (files: string[]) => void
}

export interface VitestEvents {
Expand Down
3 changes: 3 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,12 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) {

const logLevel = get<string>('logLevel', 'info')

const filesWatcherInclude = get<string>('filesWatcherInclude', '**/*')!

return {
env: get<null | Record<string, string>>('nodeEnv', null),
debugExclude: get<string[]>('debugExclude', []),
filesWatcherInclude,
vitestPackagePath: resolvedVitestPackagePath,
workspaceConfig: resolveConfigPath(workspaceConfig),
rootConfig: resolveConfigPath(rootConfigFile),
Expand Down
13 changes: 12 additions & 1 deletion src/testTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { RunnerTask, RunnerTestFile } from 'vitest'
import { TestCase, TestFile, TestFolder, TestSuite, getTestData } from './testTreeData'
import { log } from './log'
import type { VitestFolderAPI } from './api'
import { getConfig } from './config'

// testItem -> vscode.TestItem
// testData -> our wrapper
Expand Down Expand Up @@ -184,14 +185,24 @@ export class TestTree extends vscode.Disposable {
return

const watcher = vscode.workspace.createFileSystemWatcher(
new vscode.RelativePattern(api.workspaceFolder, '**/*'),
new vscode.RelativePattern(api.workspaceFolder, getConfig(api.workspaceFolder).filesWatcherInclude),
)
this.watcherByFolder.set(api.workspaceFolder, watcher)

watcher.onDidDelete((file) => {
const items = this.testItemsByFile.get(normalize(file.fsPath))
items?.forEach(item => this.recursiveDelete(item))
})

watcher.onDidChange((file) => {
const filepath = normalize(file.fsPath)
api.onFileChanged(filepath)
})

watcher.onDidCreate((file) => {
const filepath = normalize(file.fsPath)
api.onFileCreated(filepath)
})
}

private recursiveDelete(item: vscode.TestItem) {
Expand Down
1 change: 1 addition & 0 deletions src/worker/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export async function initVitest(meta: WorkerMeta, options?: UserConfig) {
{
server: {
middlewareMode: true,
watch: null,
},
plugins: [
{
Expand Down
57 changes: 57 additions & 0 deletions src/worker/vitest.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { readFileSync } from 'node:fs'
import type { Vitest as VitestCore, WorkspaceProject } from 'vitest/node'
import type { VitestMethods } from '../api/rpc'
import { VitestWatcher } from './watcher'
Expand Down Expand Up @@ -139,6 +140,62 @@ export class Vitest implements VitestMethods {
await this.ctx.report('onWatcherStart', this.ctx.state.getFiles(files))
}

private handleFileChanged(file: string): string[] {
return (this.ctx as any).handleFileChanged(file)
}

private scheduleRerun(files: string[]): Promise<void> {
return (this.ctx as any).scheduleRerun(files)
}

private updateLastChanged(filepath: string) {
const projects = this.ctx.getModuleProjects(filepath)
projects.forEach(({ server, browser }) => {
const serverMods = server.moduleGraph.getModulesByFile(filepath)
serverMods?.forEach(mod => server.moduleGraph.invalidateModule(mod))
if (browser) {
const browserMods = browser.vite.moduleGraph.getModulesByFile(filepath)
browserMods?.forEach(mod => browser.vite.moduleGraph.invalidateModule(mod))
}
})
}

onFilesChanged(files: string[]) {
try {
for (const file of files) {
const needRerun = this.handleFileChanged(file)
if (needRerun.length) {
this.updateLastChanged(file)
this.scheduleRerun(needRerun)
}
}
}
catch (err) {
console.error('Error during analyzing changed files', err)
}
}

async onFilesCreated(files: string[]) {
try {
const testFiles: string[] = []

for (const file of files) {
const content = readFileSync(file, 'utf-8')
for (const project of this.ctx.projects) {
if (await project.isTargetFile(file, content)) {
this.updateLastChanged(file)
testFiles.push(file)
}
}
}

testFiles.forEach(file => this.scheduleRerun([file]))
}
catch (err) {
console.error('Error during analyzing created files', err)
}
}

unwatchTests() {
return this.watcher.stopTracking()
}
Expand Down

0 comments on commit a815391

Please sign in to comment.