Skip to content

Commit 976f330

Browse files
committed
Watch based on dynamic polling priority frequency queue
1 parent aa5e49a commit 976f330

3 files changed

Lines changed: 223 additions & 20 deletions

File tree

src/compiler/sys.ts

Lines changed: 205 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ namespace ts {
2525
export type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind) => void;
2626
export type DirectoryWatcherCallback = (fileName: string) => void;
2727
export interface WatchedFile {
28-
fileName: string;
29-
callback: FileWatcherCallback;
30-
mtime?: Date;
28+
readonly fileName: string;
29+
readonly callback: FileWatcherCallback;
30+
mtime: Date;
3131
}
3232

3333
/* @internal */
@@ -37,7 +37,13 @@ namespace ts {
3737
Low
3838
}
3939

40-
const pollingIntervalsForPriority = [250, 1000, 4000];
40+
function getPriorityValues(highPriorityValue: number): [number, number, number] {
41+
const mediumPriorityValue = highPriorityValue * 2;
42+
const lowPriorityValue = mediumPriorityValue * 4;
43+
return [highPriorityValue, mediumPriorityValue, lowPriorityValue];
44+
}
45+
46+
const pollingIntervalsForPriority = getPriorityValues(250);
4147
function pollingInterval(watchPriority: WatchPriority): number {
4248
return pollingIntervalsForPriority[watchPriority];
4349
}
@@ -47,6 +53,201 @@ namespace ts {
4753
return host.watchFile(fileName, callback, pollingInterval(watchPriority));
4854
}
4955

56+
/* @internal */
57+
export interface DynamicPriorityPollingStatsSet {
58+
watchFile(fileName: string, callback: FileWatcherCallback, defaultPriority: WatchPriority): FileWatcher;
59+
}
60+
61+
/* @internal */
62+
export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time
63+
/* @internal */
64+
export function createDynamicPriorityPollingStatsSet(host: System): DynamicPriorityPollingStatsSet {
65+
if (!host.getModifiedTime || !host.setTimeout) {
66+
throw notImplemented();
67+
}
68+
69+
interface WatchedFile extends ts.WatchedFile {
70+
isClosed?: boolean;
71+
unchangedPolls: number;
72+
}
73+
74+
interface WatchPriorityQueue extends Array<WatchedFile> {
75+
watchPriority: WatchPriority;
76+
pollIndex: number;
77+
}
78+
79+
const chunkSizes = getPriorityValues(32);
80+
const unChangedThresholds = getPriorityValues(32);
81+
const watchedFiles: WatchedFile[] = [];
82+
const changedFilesInLastPoll: WatchedFile[] = [];
83+
const priorityQueues = [createPriorityQueue(WatchPriority.High), createPriorityQueue(WatchPriority.Medium), createPriorityQueue(WatchPriority.Low)];
84+
return {
85+
watchFile
86+
};
87+
88+
function watchFile(fileName: string, callback: FileWatcherCallback, defaultPriority: WatchPriority): FileWatcher {
89+
const file: WatchedFile = {
90+
fileName,
91+
callback,
92+
unchangedPolls: 0,
93+
mtime: getModifiedTime(fileName)
94+
};
95+
watchedFiles.push(file);
96+
97+
addToPriorityQueue(file, defaultPriority);
98+
return {
99+
close: () => {
100+
file.isClosed = true;
101+
// Remove from watchedFiles
102+
unorderedRemoveItem(watchedFiles, file);
103+
// Do not update priority queue since that will happen as part of polling
104+
}
105+
};
106+
}
107+
108+
function createPriorityQueue(watchPriority: WatchPriority): WatchPriorityQueue {
109+
const queue = [] as WatchPriorityQueue;
110+
queue.watchPriority = watchPriority;
111+
queue.pollIndex = 0;
112+
return queue;
113+
}
114+
115+
function pollPriorityQueue(queue: WatchPriorityQueue) {
116+
const priority = queue.watchPriority;
117+
queue.pollIndex = pollQueue(queue, priority, queue.pollIndex, chunkSizes[priority]);
118+
// Set the next polling index and timeout
119+
if (queue.length) {
120+
scheduleNextPoll(priority);
121+
}
122+
else {
123+
Debug.assert(queue.pollIndex === 0);
124+
}
125+
}
126+
127+
function pollHighPriorityQueue(queue: WatchPriorityQueue) {
128+
// Always poll complete list of changedFilesInLastPoll
129+
pollQueue(changedFilesInLastPoll, WatchPriority.High, /*pollIndex*/ 0, changedFilesInLastPoll.length);
130+
131+
// Finally do the actual polling of the queue
132+
pollPriorityQueue(queue);
133+
// Schedule poll if there are files in changedFilesInLastPoll but no files in the actual queue
134+
// as pollPriorityQueue wont schedule for next poll
135+
if (!queue.length && changedFilesInLastPoll.length) {
136+
scheduleNextPoll(WatchPriority.High);
137+
}
138+
}
139+
140+
function pollQueue(queue: WatchedFile[], priority: WatchPriority, pollIndex: number, chunkSize: number) {
141+
// Max visit would be all elements of the queue
142+
let needsVisit = queue.length;
143+
let definedValueCopyToIndex = pollIndex;
144+
const unChangedThreshold = unChangedThresholds[priority];
145+
for (let polled = 0; polled < chunkSize && needsVisit > 0; nextPollIndex(), needsVisit--) {
146+
const watchedFile = queue[pollIndex];
147+
if (!watchedFile) {
148+
continue;
149+
}
150+
else if (watchedFile.isClosed) {
151+
queue[pollIndex] = undefined;
152+
continue;
153+
}
154+
155+
polled++;
156+
const fileChanged = onWatchedFileStat(watchedFile, getModifiedTime(watchedFile.fileName));
157+
if (watchedFile.isClosed) {
158+
// Closed watcher as part of callback
159+
queue[pollIndex] = undefined;
160+
}
161+
else if (fileChanged) {
162+
watchedFile.unchangedPolls = 0;
163+
// Changed files go to changedFilesInLastPoll queue
164+
if (queue !== changedFilesInLastPoll && priority !== WatchPriority.High) {
165+
queue[pollIndex] = undefined;
166+
addChangedFileToHighPriorityQueue(watchedFile);
167+
}
168+
}
169+
else if (watchedFile.unchangedPolls !== unChangedThreshold) {
170+
watchedFile.unchangedPolls++;
171+
}
172+
else if (queue === changedFilesInLastPoll) {
173+
// Restart unchangedPollCount for unchanged file and move to high priority queue
174+
watchedFile.unchangedPolls = 0;
175+
addToPriorityQueue(watchedFile, WatchPriority.High);
176+
}
177+
else if (priority !== WatchPriority.Low) {
178+
watchedFile.unchangedPolls++;
179+
queue[pollIndex] = undefined;
180+
addToPriorityQueue(watchedFile, priority + 1);
181+
}
182+
183+
if (queue[pollIndex]) {
184+
// Copy this file to the non hole location
185+
if (definedValueCopyToIndex < pollIndex) {
186+
queue[definedValueCopyToIndex] = watchedFile;
187+
queue[pollIndex] = undefined;
188+
}
189+
definedValueCopyToIndex++;
190+
}
191+
}
192+
193+
// Return next poll index
194+
return pollIndex;
195+
196+
function nextPollIndex() {
197+
pollIndex++;
198+
if (pollIndex === queue.length) {
199+
if (definedValueCopyToIndex < pollIndex) {
200+
// There are holes from nextDefinedValueIndex to end of queue, change queue size
201+
queue.length = definedValueCopyToIndex;
202+
}
203+
pollIndex = 0;
204+
definedValueCopyToIndex = 0;
205+
}
206+
}
207+
}
208+
209+
function addToPriorityQueue(file: WatchedFile, priority: WatchPriority) {
210+
if (priorityQueues[priority].push(file) === 1) {
211+
scheduleNextPoll(priority);
212+
}
213+
}
214+
215+
function addChangedFileToHighPriorityQueue(file: WatchedFile) {
216+
if (changedFilesInLastPoll.push(file) === 1 && !priorityQueues[WatchPriority.High].length) {
217+
scheduleNextPoll(WatchPriority.High);
218+
}
219+
}
220+
221+
function scheduleNextPoll(priority: WatchPriority) {
222+
host.setTimeout(priority === WatchPriority.High ? pollHighPriorityQueue : pollPriorityQueue, pollingInterval(priority), priorityQueues[priority]);
223+
}
224+
225+
function getModifiedTime(fileName: string) {
226+
return host.getModifiedTime(fileName) || missingFileModifiedTime;
227+
}
228+
}
229+
230+
/**
231+
* Returns true if file status changed
232+
*/
233+
/*@internal*/
234+
export function onWatchedFileStat(watchedFile: WatchedFile, modifiedTime: Date): boolean {
235+
const oldTime = watchedFile.mtime.getTime();
236+
const newTime = modifiedTime.getTime();
237+
if (oldTime !== newTime) {
238+
watchedFile.mtime = modifiedTime;
239+
const eventKind = oldTime === 0
240+
? FileWatcherEventKind.Created
241+
: newTime === 0
242+
? FileWatcherEventKind.Deleted
243+
: FileWatcherEventKind.Changed;
244+
watchedFile.callback(watchedFile.fileName, eventKind);
245+
return true;
246+
}
247+
248+
return false;
249+
}
250+
50251
/**
51252
* Partial interface of the System thats needed to support the caching of directory structure
52253
*/

src/compiler/watchUtilities.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ namespace ts {
113113
case "PriorityPollingInterval":
114114
// Use polling interval based on priority when create watch using host.watchFile
115115
return getWatchFactoryWith(watchLogLevel, log, getDetailWatchInfo, watchFileUsingPriorityPollingInterval, watchDirectory);
116+
case "DynamicPriorityPolling":
117+
// Dynamically move frequently changing files to high frequency polling and non changing files to lower frequency
118+
return getWatchFactoryWithDynamicPriorityPolling(host, watchLogLevel, log, getDetailWatchInfo);
116119
default:
117120
return getDefaultWatchFactory(watchLogLevel, log, getDetailWatchInfo);
118121
}
@@ -143,6 +146,15 @@ namespace ts {
143146
}
144147
}
145148

149+
function getWatchFactoryWithDynamicPriorityPolling<X = undefined, Y = undefined>(host: System, watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo<X, Y>): WatchFactory<X, Y> {
150+
const pollingSet = createDynamicPriorityPollingStatsSet(host);
151+
return getWatchFactoryWith(watchLogLevel, log, getDetailWatchInfo, watchFile, watchDirectory);
152+
153+
function watchFile(_host: System, file: string, callback: FileWatcherCallback, watchPriority: WatchPriority): FileWatcher {
154+
return pollingSet.watchFile(file, callback, watchPriority);
155+
}
156+
}
157+
146158
function watchFile(host: System, file: string, callback: FileWatcherCallback, _watchPriority: WatchPriority): FileWatcher {
147159
return host.watchFile(file, callback);
148160
}
@@ -161,9 +173,9 @@ namespace ts {
161173
case WatchLogLevel.None:
162174
return addWatch;
163175
case WatchLogLevel.TriggerOnly:
164-
return createFileWatcherWithLogging;
165-
case WatchLogLevel.Verbose:
166176
return createFileWatcherWithTriggerLogging;
177+
case WatchLogLevel.Verbose:
178+
return createFileWatcherWithLogging;
167179
}
168180
}
169181

src/server/server.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -684,11 +684,11 @@ namespace ts.server {
684684
return;
685685
}
686686

687-
fs.stat(watchedFile.fileName, (err: any, stats: any) => {
687+
fs.stat(watchedFile.fileName, (err, stats) => {
688688
if (err) {
689689
if (err.code === "ENOENT") {
690690
if (watchedFile.mtime.getTime() !== 0) {
691-
watchedFile.mtime = new Date(0);
691+
watchedFile.mtime = missingFileModifiedTime;
692692
watchedFile.callback(watchedFile.fileName, FileWatcherEventKind.Deleted);
693693
}
694694
}
@@ -697,17 +697,7 @@ namespace ts.server {
697697
}
698698
}
699699
else {
700-
const oldTime = watchedFile.mtime.getTime();
701-
const newTime = stats.mtime.getTime();
702-
if (oldTime !== newTime) {
703-
watchedFile.mtime = stats.mtime;
704-
const eventKind = oldTime === 0
705-
? FileWatcherEventKind.Created
706-
: newTime === 0
707-
? FileWatcherEventKind.Deleted
708-
: FileWatcherEventKind.Changed;
709-
watchedFile.callback(watchedFile.fileName, eventKind);
710-
}
700+
onWatchedFileStat(watchedFile, stats.mtime);
711701
}
712702
});
713703
}
@@ -741,7 +731,7 @@ namespace ts.server {
741731
callback,
742732
mtime: sys.fileExists(fileName)
743733
? getModifiedTime(fileName)
744-
: new Date(0) // Any subsequent modification will occur after this time
734+
: missingFileModifiedTime // Any subsequent modification will occur after this time
745735
};
746736

747737
watchedFiles.push(file);

0 commit comments

Comments
 (0)