Path: blob/main/extensions/copilot/src/util/common/timeTravelScheduler.ts
13397 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { compareBy, numberComparator, tieBreakComparators } from '../vs/base/common/arrays';6import { onUnexpectedError } from '../vs/base/common/errors';7import { Emitter, Event } from '../vs/base/common/event';8import { Disposable, IDisposable } from '../vs/base/common/lifecycle';9import { setTimeout0, setTimeout0IsFaster } from '../vs/base/common/platform';1011export type TimeOffset = number;1213export interface Scheduler {14schedule(task: ScheduledTask): IDisposable;15get now(): TimeOffset;16}1718export interface ScheduledTask {19readonly time: TimeOffset;20readonly source: ScheduledTaskSource;2122run(): void;23}2425export interface ScheduledTaskSource {26toString(): string;27readonly stackTrace: string | undefined;28}2930interface ExtendedScheduledTask extends ScheduledTask {31id: number;32}3334const scheduledTaskComparator = tieBreakComparators<ExtendedScheduledTask>(35compareBy(i => i.time, numberComparator),36compareBy(i => i.id, numberComparator),37);3839export class TimeTravelScheduler implements Scheduler {40private taskCounter = 0;41private _now: TimeOffset = 0;42private readonly queue: PriorityQueue<ExtendedScheduledTask> = new SimplePriorityQueue<ExtendedScheduledTask>([], scheduledTaskComparator);4344private readonly taskScheduledEmitter = new Emitter<{ task: ScheduledTask }>();45public readonly onTaskScheduled = this.taskScheduledEmitter.event;4647schedule(task: ScheduledTask): IDisposable {48if (task.time < this._now) {49throw new Error(`Scheduled time (${task.time}) must be equal to or greater than the current time (${this._now}).`);50}51const extendedTask: ExtendedScheduledTask = { ...task, id: this.taskCounter++ };52this.queue.add(extendedTask);53this.taskScheduledEmitter.fire({ task });54return { dispose: () => this.queue.remove(extendedTask) };55}5657get now(): TimeOffset {58return this._now;59}6061get hasScheduledTasks(): boolean {62return this.queue.length > 0;63}6465getScheduledTasks(): readonly ScheduledTask[] {66return this.queue.toSortedArray();67}6869runNext(): ScheduledTask | undefined {70const task = this.queue.removeMin();71if (task) {72this._now = task.time;73task.run();74}7576return task;77}7879installGlobally(): IDisposable {80return overwriteGlobals(this);81}82}8384export class AsyncSchedulerProcessor extends Disposable {85private isProcessing = false;86private readonly _history = new Array<ScheduledTask>();87public get history(): readonly ScheduledTask[] { return this._history; }8889private readonly maxTaskCount: number;9091private readonly queueEmptyEmitter = new Emitter<void>();92public readonly onTaskQueueEmpty = this.queueEmptyEmitter.event;9394private lastError: Error | undefined;9596constructor(private readonly scheduler: TimeTravelScheduler, options?: { maxTaskCount?: number }) {97super();9899this.maxTaskCount = options && options.maxTaskCount ? options.maxTaskCount : 100;100101this._register(scheduler.onTaskScheduled(() => {102if (this.isProcessing) {103return;104} else {105this.isProcessing = true;106this.schedule();107}108}));109}110111private schedule() {112// This allows promises created by a previous task to settle and schedule tasks before the next task is run.113// Tasks scheduled in those promises might have to run before the current next task.114Promise.resolve().then(() => {115if ('setImmediate' in globalThis) {116// This is significantly faster when running tests.117(globalThis as any).setImmediate(() => this.process());118} else if (setTimeout0IsFaster) {119setTimeout0(() => this.process());120} else {121originalGlobalValues.setTimeout(() => this.process());122}123});124}125126private process() {127const executedTask = this.scheduler.runNext();128if (executedTask) {129this._history.push(executedTask);130131if (this.history.length >= this.maxTaskCount && this.scheduler.hasScheduledTasks) {132const lastTasks = this._history.slice(Math.max(0, this.history.length - 10)).map(h => `${h.source.toString()}: ${h.source.stackTrace}`);133const 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')}`);134this.lastError = e;135onUnexpectedError(e);136console.error(e);137throw e;138}139}140141if (this.scheduler.hasScheduledTasks) {142this.schedule();143} else {144this.isProcessing = false;145this.queueEmptyEmitter.fire();146}147}148149waitForEmptyQueue(): Promise<void> {150if (this.lastError) {151const error = this.lastError;152this.lastError = undefined;153throw error;154}155if (!this.isProcessing) {156return Promise.resolve();157} else {158return Event.toPromise(this.onTaskQueueEmpty).then(() => {159if (this.lastError) {160throw this.lastError;161}162});163}164}165}166167168export async function runWithFakedTimers<T>(options: { useFakeTimers?: boolean; maxTaskCount?: number }, fn: () => Promise<T>): Promise<T> {169const useFakeTimers = options.useFakeTimers === undefined ? true : options.useFakeTimers;170if (!useFakeTimers) {171return fn();172}173174const scheduler = new TimeTravelScheduler();175const schedulerProcessor = new AsyncSchedulerProcessor(scheduler, { maxTaskCount: options.maxTaskCount });176const globalInstallDisposable = scheduler.installGlobally();177178let result: T;179try {180result = await fn();181} finally {182globalInstallDisposable.dispose();183184try {185// We process the remaining scheduled tasks.186// The global override is no longer active, so during this, no more tasks will be scheduled.187await schedulerProcessor.waitForEmptyQueue();188} finally {189schedulerProcessor.dispose();190}191}192193return result;194}195196export const originalGlobalValues = {197setTimeout: globalThis.setTimeout.bind(globalThis),198clearTimeout: globalThis.clearTimeout.bind(globalThis),199setInterval: globalThis.setInterval.bind(globalThis),200clearInterval: globalThis.clearInterval.bind(globalThis),201Date: globalThis.Date,202};203204type TimerHandler = () => void;205206function setTimeout(scheduler: Scheduler, handler: TimerHandler, timeout: number = 0): IDisposable {207if (typeof handler === 'string') {208throw new Error('String handler args should not be used and are not supported');209}210211return scheduler.schedule({212time: scheduler.now + timeout,213run: () => {214handler();215},216source: {217toString() { return 'setTimeout'; },218stackTrace: new Error().stack,219}220});221}222223function setInterval(scheduler: Scheduler, handler: TimerHandler, interval: number): IDisposable {224if (typeof handler === 'string') {225throw new Error('String handler args should not be used and are not supported');226}227const validatedHandler = handler;228229let iterCount = 0;230const stackTrace = new Error().stack;231232let disposed = false;233let lastDisposable: IDisposable;234235function schedule(): void {236iterCount++;237const curIter = iterCount;238lastDisposable = scheduler.schedule({239time: scheduler.now + interval,240run() {241if (!disposed) {242schedule();243validatedHandler();244}245},246source: {247toString() { return `setInterval (iteration ${curIter})`; },248stackTrace,249}250});251}252253schedule();254255return {256dispose: () => {257if (disposed) {258return;259}260disposed = true;261lastDisposable.dispose();262}263};264}265266function overwriteGlobals(scheduler: Scheduler): IDisposable {267globalThis.setTimeout = ((handler: TimerHandler, timeout?: number) => setTimeout(scheduler, handler, timeout)) as any;268globalThis.clearTimeout = (timeoutId: any) => {269if (typeof timeoutId === 'object' && timeoutId && 'dispose' in timeoutId) {270timeoutId.dispose();271} else {272originalGlobalValues.clearTimeout(timeoutId);273}274};275276globalThis.setInterval = ((handler: TimerHandler, timeout: number) => setInterval(scheduler, handler, timeout)) as any;277globalThis.clearInterval = (timeoutId: any) => {278if (typeof timeoutId === 'object' && timeoutId && 'dispose' in timeoutId) {279timeoutId.dispose();280} else {281originalGlobalValues.clearInterval(timeoutId);282}283};284285globalThis.Date = createDateClass(scheduler);286287return {288dispose: () => {289Object.assign(globalThis, originalGlobalValues);290}291};292}293294function createDateClass(scheduler: Scheduler): DateConstructor {295const OriginalDate = originalGlobalValues.Date;296297function SchedulerDate(this: any, ...args: any): any {298// the Date constructor called as a function, ref Ecma-262 Edition 5.1, section 15.9.2.299// This remains so in the 10th edition of 2019 as well.300if (!(this instanceof SchedulerDate)) {301return new OriginalDate(scheduler.now).toString();302}303304// if Date is called as a constructor with 'new' keyword305if (args.length === 0) {306return new OriginalDate(scheduler.now);307}308return new (OriginalDate as any)(...args);309}310311for (const prop in OriginalDate) {312if (OriginalDate.hasOwnProperty(prop)) {313(SchedulerDate as any)[prop] = (OriginalDate as any)[prop];314}315}316317SchedulerDate.now = function now() {318return scheduler.now;319};320SchedulerDate.toString = function toString() {321return OriginalDate.toString();322};323SchedulerDate.prototype = OriginalDate.prototype;324SchedulerDate.parse = OriginalDate.parse;325SchedulerDate.UTC = OriginalDate.UTC;326SchedulerDate.prototype.toUTCString = OriginalDate.prototype.toUTCString;327328return SchedulerDate as any;329}330331interface PriorityQueue<T> {332length: number;333add(value: T): void;334remove(value: T): void;335336removeMin(): T | undefined;337toSortedArray(): T[];338}339340class SimplePriorityQueue<T> implements PriorityQueue<T> {341private isSorted = false;342private items: T[];343344constructor(items: T[], private readonly compare: (a: T, b: T) => number) {345this.items = items;346}347348get length(): number {349return this.items.length;350}351352add(value: T): void {353this.items.push(value);354this.isSorted = false;355}356357remove(value: T): void {358const idx = this.items.indexOf(value);359if (idx !== -1) {360this.items.splice(idx, 1);361this.isSorted = false;362}363}364365removeMin(): T | undefined {366this.ensureSorted();367return this.items.shift();368}369370getMin(): T | undefined {371this.ensureSorted();372return this.items[0];373}374375toSortedArray(): T[] {376this.ensureSorted();377return [...this.items];378}379380private ensureSorted() {381if (!this.isSorted) {382this.items.sort(this.compare);383this.isSorted = true;384}385}386}387388389