Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/util/common/timeTravelScheduler.ts
13397 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { compareBy, numberComparator, tieBreakComparators } from '../vs/base/common/arrays';
7
import { onUnexpectedError } from '../vs/base/common/errors';
8
import { Emitter, Event } from '../vs/base/common/event';
9
import { Disposable, IDisposable } from '../vs/base/common/lifecycle';
10
import { setTimeout0, setTimeout0IsFaster } from '../vs/base/common/platform';
11
12
export type TimeOffset = number;
13
14
export interface Scheduler {
15
schedule(task: ScheduledTask): IDisposable;
16
get now(): TimeOffset;
17
}
18
19
export interface ScheduledTask {
20
readonly time: TimeOffset;
21
readonly source: ScheduledTaskSource;
22
23
run(): void;
24
}
25
26
export interface ScheduledTaskSource {
27
toString(): string;
28
readonly stackTrace: string | undefined;
29
}
30
31
interface ExtendedScheduledTask extends ScheduledTask {
32
id: number;
33
}
34
35
const scheduledTaskComparator = tieBreakComparators<ExtendedScheduledTask>(
36
compareBy(i => i.time, numberComparator),
37
compareBy(i => i.id, numberComparator),
38
);
39
40
export class TimeTravelScheduler implements Scheduler {
41
private taskCounter = 0;
42
private _now: TimeOffset = 0;
43
private readonly queue: PriorityQueue<ExtendedScheduledTask> = new SimplePriorityQueue<ExtendedScheduledTask>([], scheduledTaskComparator);
44
45
private readonly taskScheduledEmitter = new Emitter<{ task: ScheduledTask }>();
46
public readonly onTaskScheduled = this.taskScheduledEmitter.event;
47
48
schedule(task: ScheduledTask): IDisposable {
49
if (task.time < this._now) {
50
throw new Error(`Scheduled time (${task.time}) must be equal to or greater than the current time (${this._now}).`);
51
}
52
const extendedTask: ExtendedScheduledTask = { ...task, id: this.taskCounter++ };
53
this.queue.add(extendedTask);
54
this.taskScheduledEmitter.fire({ task });
55
return { dispose: () => this.queue.remove(extendedTask) };
56
}
57
58
get now(): TimeOffset {
59
return this._now;
60
}
61
62
get hasScheduledTasks(): boolean {
63
return this.queue.length > 0;
64
}
65
66
getScheduledTasks(): readonly ScheduledTask[] {
67
return this.queue.toSortedArray();
68
}
69
70
runNext(): ScheduledTask | undefined {
71
const task = this.queue.removeMin();
72
if (task) {
73
this._now = task.time;
74
task.run();
75
}
76
77
return task;
78
}
79
80
installGlobally(): IDisposable {
81
return overwriteGlobals(this);
82
}
83
}
84
85
export class AsyncSchedulerProcessor extends Disposable {
86
private isProcessing = false;
87
private readonly _history = new Array<ScheduledTask>();
88
public get history(): readonly ScheduledTask[] { return this._history; }
89
90
private readonly maxTaskCount: number;
91
92
private readonly queueEmptyEmitter = new Emitter<void>();
93
public readonly onTaskQueueEmpty = this.queueEmptyEmitter.event;
94
95
private lastError: Error | undefined;
96
97
constructor(private readonly scheduler: TimeTravelScheduler, options?: { maxTaskCount?: number }) {
98
super();
99
100
this.maxTaskCount = options && options.maxTaskCount ? options.maxTaskCount : 100;
101
102
this._register(scheduler.onTaskScheduled(() => {
103
if (this.isProcessing) {
104
return;
105
} else {
106
this.isProcessing = true;
107
this.schedule();
108
}
109
}));
110
}
111
112
private schedule() {
113
// This allows promises created by a previous task to settle and schedule tasks before the next task is run.
114
// Tasks scheduled in those promises might have to run before the current next task.
115
Promise.resolve().then(() => {
116
if ('setImmediate' in globalThis) {
117
// This is significantly faster when running tests.
118
(globalThis as any).setImmediate(() => this.process());
119
} else if (setTimeout0IsFaster) {
120
setTimeout0(() => this.process());
121
} else {
122
originalGlobalValues.setTimeout(() => this.process());
123
}
124
});
125
}
126
127
private process() {
128
const executedTask = this.scheduler.runNext();
129
if (executedTask) {
130
this._history.push(executedTask);
131
132
if (this.history.length >= this.maxTaskCount && this.scheduler.hasScheduledTasks) {
133
const lastTasks = this._history.slice(Math.max(0, this.history.length - 10)).map(h => `${h.source.toString()}: ${h.source.stackTrace}`);
134
const e = new Error(`Queue did not get empty after processing ${this.history.length} items. These are the last ${lastTasks.length} scheduled tasks:\n${lastTasks.join('\n\n\n')}`);
135
this.lastError = e;
136
onUnexpectedError(e);
137
console.error(e);
138
throw e;
139
}
140
}
141
142
if (this.scheduler.hasScheduledTasks) {
143
this.schedule();
144
} else {
145
this.isProcessing = false;
146
this.queueEmptyEmitter.fire();
147
}
148
}
149
150
waitForEmptyQueue(): Promise<void> {
151
if (this.lastError) {
152
const error = this.lastError;
153
this.lastError = undefined;
154
throw error;
155
}
156
if (!this.isProcessing) {
157
return Promise.resolve();
158
} else {
159
return Event.toPromise(this.onTaskQueueEmpty).then(() => {
160
if (this.lastError) {
161
throw this.lastError;
162
}
163
});
164
}
165
}
166
}
167
168
169
export async function runWithFakedTimers<T>(options: { useFakeTimers?: boolean; maxTaskCount?: number }, fn: () => Promise<T>): Promise<T> {
170
const useFakeTimers = options.useFakeTimers === undefined ? true : options.useFakeTimers;
171
if (!useFakeTimers) {
172
return fn();
173
}
174
175
const scheduler = new TimeTravelScheduler();
176
const schedulerProcessor = new AsyncSchedulerProcessor(scheduler, { maxTaskCount: options.maxTaskCount });
177
const globalInstallDisposable = scheduler.installGlobally();
178
179
let result: T;
180
try {
181
result = await fn();
182
} finally {
183
globalInstallDisposable.dispose();
184
185
try {
186
// We process the remaining scheduled tasks.
187
// The global override is no longer active, so during this, no more tasks will be scheduled.
188
await schedulerProcessor.waitForEmptyQueue();
189
} finally {
190
schedulerProcessor.dispose();
191
}
192
}
193
194
return result;
195
}
196
197
export const originalGlobalValues = {
198
setTimeout: globalThis.setTimeout.bind(globalThis),
199
clearTimeout: globalThis.clearTimeout.bind(globalThis),
200
setInterval: globalThis.setInterval.bind(globalThis),
201
clearInterval: globalThis.clearInterval.bind(globalThis),
202
Date: globalThis.Date,
203
};
204
205
type TimerHandler = () => void;
206
207
function setTimeout(scheduler: Scheduler, handler: TimerHandler, timeout: number = 0): IDisposable {
208
if (typeof handler === 'string') {
209
throw new Error('String handler args should not be used and are not supported');
210
}
211
212
return scheduler.schedule({
213
time: scheduler.now + timeout,
214
run: () => {
215
handler();
216
},
217
source: {
218
toString() { return 'setTimeout'; },
219
stackTrace: new Error().stack,
220
}
221
});
222
}
223
224
function setInterval(scheduler: Scheduler, handler: TimerHandler, interval: number): IDisposable {
225
if (typeof handler === 'string') {
226
throw new Error('String handler args should not be used and are not supported');
227
}
228
const validatedHandler = handler;
229
230
let iterCount = 0;
231
const stackTrace = new Error().stack;
232
233
let disposed = false;
234
let lastDisposable: IDisposable;
235
236
function schedule(): void {
237
iterCount++;
238
const curIter = iterCount;
239
lastDisposable = scheduler.schedule({
240
time: scheduler.now + interval,
241
run() {
242
if (!disposed) {
243
schedule();
244
validatedHandler();
245
}
246
},
247
source: {
248
toString() { return `setInterval (iteration ${curIter})`; },
249
stackTrace,
250
}
251
});
252
}
253
254
schedule();
255
256
return {
257
dispose: () => {
258
if (disposed) {
259
return;
260
}
261
disposed = true;
262
lastDisposable.dispose();
263
}
264
};
265
}
266
267
function overwriteGlobals(scheduler: Scheduler): IDisposable {
268
globalThis.setTimeout = ((handler: TimerHandler, timeout?: number) => setTimeout(scheduler, handler, timeout)) as any;
269
globalThis.clearTimeout = (timeoutId: any) => {
270
if (typeof timeoutId === 'object' && timeoutId && 'dispose' in timeoutId) {
271
timeoutId.dispose();
272
} else {
273
originalGlobalValues.clearTimeout(timeoutId);
274
}
275
};
276
277
globalThis.setInterval = ((handler: TimerHandler, timeout: number) => setInterval(scheduler, handler, timeout)) as any;
278
globalThis.clearInterval = (timeoutId: any) => {
279
if (typeof timeoutId === 'object' && timeoutId && 'dispose' in timeoutId) {
280
timeoutId.dispose();
281
} else {
282
originalGlobalValues.clearInterval(timeoutId);
283
}
284
};
285
286
globalThis.Date = createDateClass(scheduler);
287
288
return {
289
dispose: () => {
290
Object.assign(globalThis, originalGlobalValues);
291
}
292
};
293
}
294
295
function createDateClass(scheduler: Scheduler): DateConstructor {
296
const OriginalDate = originalGlobalValues.Date;
297
298
function SchedulerDate(this: any, ...args: any): any {
299
// the Date constructor called as a function, ref Ecma-262 Edition 5.1, section 15.9.2.
300
// This remains so in the 10th edition of 2019 as well.
301
if (!(this instanceof SchedulerDate)) {
302
return new OriginalDate(scheduler.now).toString();
303
}
304
305
// if Date is called as a constructor with 'new' keyword
306
if (args.length === 0) {
307
return new OriginalDate(scheduler.now);
308
}
309
return new (OriginalDate as any)(...args);
310
}
311
312
for (const prop in OriginalDate) {
313
if (OriginalDate.hasOwnProperty(prop)) {
314
(SchedulerDate as any)[prop] = (OriginalDate as any)[prop];
315
}
316
}
317
318
SchedulerDate.now = function now() {
319
return scheduler.now;
320
};
321
SchedulerDate.toString = function toString() {
322
return OriginalDate.toString();
323
};
324
SchedulerDate.prototype = OriginalDate.prototype;
325
SchedulerDate.parse = OriginalDate.parse;
326
SchedulerDate.UTC = OriginalDate.UTC;
327
SchedulerDate.prototype.toUTCString = OriginalDate.prototype.toUTCString;
328
329
return SchedulerDate as any;
330
}
331
332
interface PriorityQueue<T> {
333
length: number;
334
add(value: T): void;
335
remove(value: T): void;
336
337
removeMin(): T | undefined;
338
toSortedArray(): T[];
339
}
340
341
class SimplePriorityQueue<T> implements PriorityQueue<T> {
342
private isSorted = false;
343
private items: T[];
344
345
constructor(items: T[], private readonly compare: (a: T, b: T) => number) {
346
this.items = items;
347
}
348
349
get length(): number {
350
return this.items.length;
351
}
352
353
add(value: T): void {
354
this.items.push(value);
355
this.isSorted = false;
356
}
357
358
remove(value: T): void {
359
const idx = this.items.indexOf(value);
360
if (idx !== -1) {
361
this.items.splice(idx, 1);
362
this.isSorted = false;
363
}
364
}
365
366
removeMin(): T | undefined {
367
this.ensureSorted();
368
return this.items.shift();
369
}
370
371
getMin(): T | undefined {
372
this.ensureSorted();
373
return this.items[0];
374
}
375
376
toSortedArray(): T[] {
377
this.ensureSorted();
378
return [...this.items];
379
}
380
381
private ensureSorted() {
382
if (!this.isSorted) {
383
this.items.sort(this.compare);
384
this.isSorted = true;
385
}
386
}
387
}
388
389