Watcher performance improvements#4399
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves watch-mode performance in the internal/execute watcher by reusing an existing single-file Program via UpdateProgram when safe, and introducing a watchSetDirty flag to avoid unnecessary work unless a full watch set rebuild is required.
Changes:
- Add
watchSetDirtystate to force full rebuilds only when directory/wildcard inclusion changes are suspected. - Introduce a fast path that attempts
Program.UpdateProgramreuse when exactly one file changed and config/watch set are unchanged. - Reduce unnecessary wildcard file-name reload operations during rebuilds.
|
Do you have any data about how this affects things? |
In my testing single-file edits show the biggest performance gain, and the gain scales with project size. I had copilot run a benchmark in |
|
I think this has two correctness regressions in the watcher fast path. I was able to reduce both to focused tests: func TestWatcherStartsFromExistingBuildInfo(t *testing.T) {
input := &tscInput{
files: FileMap{
"/home/src/workspaces/project/index.ts": `export const x: number = 1;`,
"/home/src/workspaces/project/tsconfig.json": `{"compilerOptions":{"composite":true},"files":["index.ts"]}`,
},
}
sys := newTestSys(input, false)
result := execute.CommandLine(context.Background(), sys, []string{"-p", "tsconfig.json", "--pretty", "false"}, sys)
assert.Equal(t, result.Status, tsc.ExitStatusSuccess)
assert.Assert(t, sys.fsFromFileMap().FileExists("/home/src/workspaces/project/tsconfig.tsbuildinfo"))
sys.clearOutput()
defer func() {
if r := recover(); r != nil {
t.Fatalf("watch startup with existing build info panicked: %v", r)
}
}()
result = execute.CommandLine(context.Background(), sys, []string{"--watch", "--noEmit", "--pretty", "false"}, sys)
assert.Equal(t, result.Status, tsc.ExitStatusSuccess)
assert.Assert(t, result.Watcher != nil)
}
func TestWatcherRebuildsWhenJsxImportSourcePragmaChanges(t *testing.T) {
input := &tscInput{
files: FileMap{
"/home/src/workspaces/project/index.tsx": `/** @jsxImportSource foo */
export const x = <div />;`,
"/home/src/workspaces/project/tsconfig.json": `{
"compilerOptions":{"jsx":"react-jsx","module":"esnext","moduleResolution":"bundler","noEmit":true},
"files":["index.tsx"]
}`,
},
commandLineArgs: []string{"--watch"},
}
sys := newTestSys(input, false)
result := execute.CommandLine(context.Background(), sys, []string{"--watch", "--pretty", "false"}, sys)
if result.Watcher == nil {
t.Fatal("expected Watcher to be non-nil in watch mode")
}
w := result.Watcher.(*execute.Watcher)
sys.currentWrite.Reset()
_ = sys.fsFromFileMap().WriteFile("/home/src/workspaces/project/index.tsx", `/** @jsxImportSource bar */
export const x = <div />;`)
sys.mockWatchBackend.SendEvents([]fswatch.Event{
{Kind: fswatch.EventUpdate, Path: "/home/src/workspaces/project/index.tsx"},
})
w.DoCycle()
out := sys.currentWrite.String()
assert.Assert(t, strings.Contains(out, "bar/jsx-runtime"), "expected updated JSX runtime diagnostic, got: %s", out)
assert.Assert(t, !strings.Contains(out, "foo/jsx-runtime"), "expected stale JSX runtime diagnostic to be gone, got: %s", out)
}On this branch: go test -run 'TestWatcher(StartsFromExistingBuildInfo|RebuildsWhenJsxImportSourcePragmaChanges)$' ./internal/execute/tsctestsfails with: watch startup with existing build info panicked: GetProgram: should not be called without programand the JSX test continues reporting The first one looks like the new watcher fast path is trying |
Added single-file program reuse via
UpdateProgramand skips other unnecessary operations. Also keeps awatchSetDirtyflag to track when a full rebuild is required.