All files / utilities scheduling.ts

100% Statements 15/15
100% Branches 7/7
100% Functions 6/6
100% Lines 12/12

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72                                                        103x   103x   134x 113x     21x   21x           21x 7x     21x                                 32x 1x     31x    
/**
 * Lets CPU-bound loops on the extension host hand the event loop a turn.
 *
 * All parsing, inference and validation runs on the extension-host thread, so a
 * tight loop over many files starves the event loop: the status bar freezes,
 * webviews and trees stop responding and provider requests time out. Awaiting a
 * resolved promise only drains microtasks, which is not enough to unblock the
 * renderer — a macrotask must be scheduled. Yielding after every item would pay
 * a timer per (possibly tiny) item, so the yielder yields one macrotask whenever
 * more than the configured wall-clock time has elapsed since the last yield.
 */
export interface EventLoopYielder {
	/**
	 * Yields control to the event loop when the work has been busy for longer
	 * than the wall-clock budget since the last yield; otherwise resolves
	 * immediately. Yields a second time when other work kept the event loop
	 * busy for longer than the budget during the first yield.
	 */
	yieldIfBusy(): Promise<void>;
}
 
/**
 * Creates an {@link EventLoopYielder} for one loop or batch run. A yielder tracks
 * the time of its last yield, so create a fresh one per run instead of sharing.
 * @param budgetMs Wall-clock milliseconds of uninterrupted work allowed between
 * yields. The 50 ms default keeps the UI at ~20 paints/s during a batch.
 */
export function createEventLoopYielder(budgetMs: number = 50): EventLoopYielder {
	let lastYield = Date.now();
 
	return {
		async yieldIfBusy(): Promise<void> {
			if (Date.now() - lastYield <= budgetMs) {
				return;
			}
 
			const yieldStart = Date.now();
 
			await yieldToEventLoop();
 
			// Other work that was waiting for the same turn (e.g. the diagnostics of a
			// document that was just parsed) may have run for longer than the budget
			// after the event loop polled for I/O. Yield once more, so that requests
			// which arrived meanwhile are not stuck behind the next phase as well.
			if (Date.now() - yieldStart > budgetMs) {
				await yieldToEventLoop();
			}
 
			lastYield = Date.now();
		}
	};
}
 
/**
 * Resolves once the event loop has polled for I/O, so that requests which
 * arrived while the caller was busy (e.g. a go to definition request from the
 * workbench) are handled before the caller continues.
 *
 * A `setTimeout(0)` callback that is already due runs in the timers phase,
 * which Node.js processes before polling for I/O, and a single `setImmediate`
 * scheduled from an I/O callback runs in the check phase right after it. Two
 * `setImmediate` hops always span a poll phase. Where `setImmediate` does not
 * exist (the web extension host), a zero-delay timeout is used.
 */
export function yieldToEventLoop(): Promise<void> {
	if (typeof setImmediate !== 'function') {
		return new Promise(resolve => setTimeout(resolve, 0));
	}
 
	return new Promise(resolve => setImmediate(() => setImmediate(resolve)));
}