Skip to content

Delay "File change detected" reporting until createProgram #47427

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 19, 2022

Conversation

jakebailey
Copy link
Member

There are cases where the file/directory watching handler can't determine whether or not a change should be ignored or not. For files with unsupported extensions (like .txt), the watcher never calls the handler. However, extensionless files do cause the watcher to call the handler (among other cases).

Rather than trying to perfect the watcher logic, delay printing of the "file change detected" message until we actually know we're going to create a new program (i.e. after it's determined that something important changed, and therefore a new program needs to be created).

Fixes #46949

@typescript-bot typescript-bot added Author: Team For Milestone Bug PRs that fix a bug with a specific milestone labels Jan 13, 2022
Copy link
Member

@DanielRosenwasser DanielRosenwasser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State management seems rough here. The solution seems okay, but maybe others on the team have better strategies.

@jakebailey
Copy link
Member Author

jakebailey commented Jan 14, 2022

If this is less desirable, an alternative change that I considered was to, in watchWildcardDirectory, pass the result of addOrDeleteFileOrDirectory to isIgnoredFileFromWildCardWatching, then have it explicitly ignore extensionless files (using the cached stat).

But, I feel like there are other cases that are not handled there either, e.g. consider a new file that is supported, or a new directory, but that aren't actually in the program (thanks to exclude or not being in include or something). In that case, this same bug would happen, as the file watcher would trigger, but the program would not be recreated and never log the second message. To avoid that, I thought it would be a little better to just tie this to the actual thing that prints the second message, the creation of a new program.

FWIW, changesAffectResolution is no worse when it comes to state management; it's also a value that gets set by some caller of synchronizeProgram, then is cleared when the synchronize is finished.

Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't use watch mode very often, so I may be out of touch, but I think I'd rather see "detected change... ignoring change" than nothing if it's actually doing work.

@@ -434,15 +435,22 @@ namespace ts {
const hasInvalidatedResolution = resolutionCache.createHasInvalidatedResolution(userProvidedResolution || changesAffectResolution);
if (isProgramUptoDate(getCurrentProgram(), rootFileNames, compilerOptions, getSourceVersion, fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames, getParsedCommandLine, projectReferences)) {
if (hasChangedConfigFileParsingErrors) {
if (reportWatchDiagnosticOnCreateProgram) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this has to be true? How could the config file change without triggering a program refresh?

Copy link
Member Author

@jakebailey jakebailey Jan 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I'm mistaken, hasChangedConfigFileParsingErrors can be true if reloadFileNamesFromConfigFile was called, which can happen if the refresh level was ConfigFileProgramReloadLevel.Partial (and then those call sites are outside of the watcher changes, entirely, I think). I'll remove the if and see if there are any tests for this, though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like that if there's a case where hasChangedConfigFileParsingErrors is true but reportWatchDiagnosticOnCreateProgram is false, there's no test for it.

I can change this to unconditionally report the message, but I'm not entirely confident that there's no code path where this breaks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar from this code - I'm just pointing out that, abstractly, it seems like a config file change will always be a relevant file change (i.e. will require the message to be printed).

@amcasey
Copy link
Member

amcasey commented Jan 15, 2022

@jakebailey Do you have a link to the kick-out that's causing the problem (i.e. preventing the second log message from being printed)?

@jakebailey
Copy link
Member Author

I believe it's

.

(I can check when I have a chance and I'm back at my machine)

@jakebailey
Copy link
Member Author

I think I'd rather see "detected change... ignoring change" than nothing if it's actually doing work.

This is a little tricky due to how this thing is printed; it goes via afterProgramCreate below which is a result of the emitFilesAndReportErrors callback.

@amcasey
Copy link
Member

amcasey commented Jan 18, 2022

I believe it's

.
(I can check when I have a chance and I'm back at my machine)

false doesn't mean it's not ignored?

Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd rather see "detected change... ignoring change" than nothing if it's actually doing work.

This is a little tricky due to how this thing is printed; it goes via afterProgramCreate below which is a result of the emitFilesAndReportErrors callback.

I might be missing something obvious, but wouldn't it just be a matter of updating synchronizeProgram to print "ignoring" in cases where this PR does not print "change detected"?

createNewProgram(hasInvalidatedResolution);
}

changesAffectResolution = false; // reset for next sync
reportFileChangeDetectedOnCreateProgram = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be cleared in updateProgramWithWatchStatus? I think that would make its lifetime/scope much clearer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can clear it there instead, but I had been modelling changesAffectResolution.

My main concern was a case where it may be left over for another synchronize later, but I don't know why I thought that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I think it's clearer to set it and call it in the same place, but I'm also fine with following local conventions. If this is where things are cleared after sync, then go for it.

@@ -434,15 +435,22 @@ namespace ts {
const hasInvalidatedResolution = resolutionCache.createHasInvalidatedResolution(userProvidedResolution || changesAffectResolution);
if (isProgramUptoDate(getCurrentProgram(), rootFileNames, compilerOptions, getSourceVersion, fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames, getParsedCommandLine, projectReferences)) {
if (hasChangedConfigFileParsingErrors) {
if (reportFileChangeDetectedOnCreateProgram) {
reportWatchDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much work is done between where this used to be reported and here? I'm concerned that it will look like the compiler is frozen if it reacts "late" to a file change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good question; my skimming says that isProgramUptoDate will:

  • Compare a few arrays for equality
  • Check for not-up-to-date source files, which appears to be checking a number of caches
  • Check for files that the program thought were missing on disk
  • Compare compiler options
  • Compare tsconfig contents by text for position changes for diagnostics

I'm not sure that it's a huge amount of work.

I could try and have it unconditionally run afterProgramCreate, but based on #37266, it seems like this was an intentional change (but, hard to tell entirely, given it's one part of a lot of other tweaks). I'll try it and see if anything breaks catastrophically, but I don't know exactly how to double check that I don't undo a perf fix.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we convinced ourselves that it should be cheap. I'm fine bugging this into shape if we get complaints.

@jakebailey
Copy link
Member Author

false doesn't mean it's not ignored?

It does mean it's not ignored (so the watcher will emit the event), but in this case, adding the extensionless file doesn't actually cause the program to change.

I debugged and I did note the correct return statement.

It occurs to me that my alternative fix (using the stat info) is likely to not work right when that same extensionless file is deleted later, since we won't have any idea if that was a directory or not that changed.

@amcasey
Copy link
Member

amcasey commented Jan 19, 2022

@jakebailey's offline explanation - we can't break existing consumers by changing our output - convinced me that printing an explicit ignore message would be worse.

@@ -84,9 +84,6 @@ Input::
//// [/a/b/tsconfig.json] deleted

Output::
>> Screen clear
[12:00:24 AM] File change detected. Starting incremental compilation...

error TS5083: Cannot read file '/a/b/tsconfig.json'.
Copy link
Member Author

@jakebailey jakebailey Jan 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's hard to tell in the diff here, but after this message is printed, the process exits (see exitCode below), so it's sort of pointless if this message is printed or not here other than to maybe indicate that we noticed because of a file watcher update.

This same error is printed if we start with a missing config file in the first place.

Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for investigating!

@jakebailey jakebailey merged commit 97017ee into microsoft:main Jan 19, 2022
@jakebailey jakebailey deleted the fix-46949-2 branch January 19, 2022 02:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Milestone Bug PRs that fix a bug with a specific milestone
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

tsc -watch should ignore files without extensions, pretends to hang during compilation instead
4 participants