Skip to content
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

Add WindowIdleValue and GlobalIdleValue, likewise runWhenGlobalIdle and runWhenWindowIdle. #197721

Merged
merged 3 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/typings/base-common.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

// Declare types that we probe for to implement util and/or polyfill functions

declare global {

interface IdleDeadline {
readonly didTimeout: boolean;
timeRemaining(): number;
}

function requestIdleCallback(callback: (args: IdleDeadline) => void, options?: { timeout: number }): number;
function cancelIdleCallback(handle: number): void;

}

export { }
37 changes: 36 additions & 1 deletion src/vs/base/browser/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as browser from 'vs/base/browser/browser';
import { BrowserFeatures } from 'vs/base/browser/canIUse';
import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { TimeoutTimer } from 'vs/base/common/async';
import { AbstractIdleValue, TimeoutTimer, _runWhenIdle, IdleDeadline } from 'vs/base/common/async';
import { onUnexpectedError } from 'vs/base/common/errors';
import * as event from 'vs/base/common/event';
import * as dompurify from 'vs/base/browser/dompurify/dompurify';
Expand Down Expand Up @@ -178,6 +178,41 @@ export function addDisposableGenericMouseUpListener(node: EventTarget, handler:
return addDisposableListener(node, platform.isIOS && BrowserFeatures.pointerEvents ? EventType.POINTER_UP : EventType.MOUSE_UP, handler, useCapture);
}


/**
* Execute the callback the next time the browser is idle, returning an
* {@link IDisposable} that will cancel the callback when disposed. This wraps
* [requestIdleCallback] so it will fallback to [setTimeout] if the environment
* doesn't support it.
*
* @param targetWindow The window for which to run the idle callback
* @param callback The callback to run when idle, this includes an
* [IdleDeadline] that provides the time alloted for the idle callback by the
* browser. Not respecting this deadline will result in a degraded user
* experience.
* @param timeout A timeout at which point to queue no longer wait for an idle
* callback but queue it on the regular event loop (like setTimeout). Typically
* this should not be used.
*
* [IdleDeadline]: https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline
* [requestIdleCallback]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
* [setTimeout]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout
*/
export function runWhenWindowIdle(targetWindow: Window | typeof globalThis, callback: (idle: IdleDeadline) => void, timeout?: number): IDisposable {
return _runWhenIdle(targetWindow, callback, timeout);
}

/**
* An implementation of the "idle-until-urgent"-strategy as introduced
* here: https://philipwalton.com/articles/idle-until-urgent/
*/
export class WindowIdleValue<T> extends AbstractIdleValue<T> {
constructor(targetWindow: Window | typeof globalThis, executor: () => T) {
super(targetWindow, executor);
}
}


/**
* Schedule a callback to be run at the next animation frame.
* This allows multiple parties to register callbacks that should run at the next animation frame.
Expand Down
46 changes: 23 additions & 23 deletions src/vs/base/common/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,9 @@ export interface IdleDeadline {
timeRemaining(): number;
}

type IdleApi = Pick<typeof globalThis, 'requestIdleCallback' | 'cancelIdleCallback'>;


/**
* Execute the callback the next time the browser is idle, returning an
* {@link IDisposable} that will cancel the callback when disposed. This wraps
Expand All @@ -1236,31 +1239,29 @@ export interface IdleDeadline {
* [IdleDeadline]: https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline
* [requestIdleCallback]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
* [setTimeout]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout
*
* **Note** that there is `dom.ts#runWhenWindowIdle` which is better suited when running inside a browser
* context
*/
export let runWhenIdle: (targetWindow: typeof globalThis, callback: (idle: IdleDeadline) => void, timeout?: number) => IDisposable;

/**
* @deprecated use `runWhenIdle` instead passing in the target window
*/
export let globalRunWhenIdle: (callback: (idle: IdleDeadline) => void, timeout?: number) => IDisposable;
export let runWhenGlobalIdle: (callback: (idle: IdleDeadline) => void, timeout?: number) => IDisposable;

declare function requestIdleCallback(callback: (args: IdleDeadline) => void, options?: { timeout: number }): number;
declare function cancelIdleCallback(handle: number): void;
export let _runWhenIdle: (targetWindow: IdleApi, callback: (idle: IdleDeadline) => void, timeout?: number) => IDisposable;

(function () {
if (typeof requestIdleCallback !== 'function' || typeof cancelIdleCallback !== 'function') {
runWhenIdle = (targetWindow, runner) => {
if (typeof globalThis.requestIdleCallback !== 'function' || typeof globalThis.cancelIdleCallback !== 'function') {
_runWhenIdle = (_targetWindow, runner) => {
setTimeout0(() => {
if (disposed) {
return;
}
const end = Date.now() + 15; // one frame at 64fps
runner(Object.freeze({
const deadline: IdleDeadline = {
didTimeout: true,
timeRemaining() {
return Math.max(0, end - Date.now());
}
}));
};
runner(Object.freeze(deadline));
});
let disposed = false;
return {
Expand All @@ -1273,7 +1274,7 @@ declare function cancelIdleCallback(handle: number): void;
};
};
} else {
runWhenIdle = (targetWindow: any, runner, timeout?) => {
_runWhenIdle = (targetWindow: IdleApi, runner, timeout?) => {
const handle: number = targetWindow.requestIdleCallback(runner, typeof timeout === 'number' ? { timeout } : undefined);
let disposed = false;
return {
Expand All @@ -1287,14 +1288,10 @@ declare function cancelIdleCallback(handle: number): void;
};
};
}
globalRunWhenIdle = (runner) => runWhenIdle(globalThis, runner);
runWhenGlobalIdle = (runner) => _runWhenIdle(globalThis, runner);
})();

/**
* An implementation of the "idle-until-urgent"-strategy as introduced
* here: https://philipwalton.com/articles/idle-until-urgent/
*/
export class IdleValue<T> {
export abstract class AbstractIdleValue<T> {

private readonly _executor: () => void;
private readonly _handle: IDisposable;
Expand All @@ -1303,7 +1300,7 @@ export class IdleValue<T> {
private _value?: T;
private _error: unknown;

constructor(targetWindow: typeof globalThis, executor: () => T) {
constructor(targetWindow: IdleApi, executor: () => T) {
this._executor = () => {
try {
this._value = executor();
Expand All @@ -1313,7 +1310,7 @@ export class IdleValue<T> {
this._didRun = true;
}
};
this._handle = runWhenIdle(targetWindow, () => this._executor());
this._handle = _runWhenIdle(targetWindow, () => this._executor());
}

dispose(): void {
Expand All @@ -1337,9 +1334,12 @@ export class IdleValue<T> {
}

/**
* @deprecated use `IdleValue` instead passing in the target window
* An `IdleValue` that always uses the current window (which might be throttled or inactive)
*
* **Note** that there is `dom.ts#WindowIdleValue` which is better suited when running inside a browser
* context
*/
export class GlobalIdleValue<T> extends IdleValue<T> {
export class GlobalIdleValue<T> extends AbstractIdleValue<T> {

constructor(executor: () => T) {
super(globalThis, executor);
Expand Down
4 changes: 2 additions & 2 deletions src/vs/code/electron-main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ import { IExtensionsScannerService } from 'vs/platform/extensionManagement/commo
import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService';
import { UserDataProfilesHandler } from 'vs/platform/userDataProfile/electron-main/userDataProfilesHandler';
import { ProfileStorageChangesListenerChannel } from 'vs/platform/userDataProfile/electron-main/userDataProfileStorageIpc';
import { Promises, RunOnceScheduler, runWhenIdle } from 'vs/base/common/async';
import { Promises, RunOnceScheduler, runWhenGlobalIdle } from 'vs/base/common/async';
import { resolveMachineId, resolveSqmId } from 'vs/platform/telemetry/electron-main/telemetryUtils';
import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/node/extensionsProfileScannerService';
import { LoggerChannel } from 'vs/platform/log/electron-main/logIpc';
Expand Down Expand Up @@ -624,7 +624,7 @@ export class CodeApplication extends Disposable {

// Set lifecycle phase to `Eventually` after a short delay and when idle (min 2.5sec, max 5sec)
const eventuallyPhaseScheduler = this._register(new RunOnceScheduler(() => {
this._register(runWhenIdle(globalThis, () => this.lifecycleMainService.phase = LifecycleMainPhase.Eventually, 2500));
this._register(runWhenGlobalIdle(() => this.lifecycleMainService.phase = LifecycleMainPhase.Eventually, 2500));
}, 2500));
eventuallyPhaseScheduler.schedule();
}
Expand Down
11 changes: 5 additions & 6 deletions src/vs/editor/browser/widget/codeEditorContributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { getWindow } from 'vs/base/browser/dom';
import { runWhenIdle } from 'vs/base/common/async';
import { getWindow, runWhenWindowIdle } from 'vs/base/browser/dom';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Disposable, DisposableMap } from 'vs/base/common/lifecycle';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
Expand Down Expand Up @@ -58,21 +57,21 @@ export class CodeEditorContributions extends Disposable {
// AfterFirstRender
// - these extensions will be instantiated at the latest 50ms after the first render.
// - but if there is idle time, we will instantiate them sooner.
this._register(runWhenIdle(getWindow(this._editor.getDomNode()), () => {
this._register(runWhenWindowIdle(getWindow(this._editor.getDomNode()), () => {
this._instantiateSome(EditorContributionInstantiation.AfterFirstRender);
}));

// BeforeFirstInteraction
// - these extensions will be instantiated at the latest before a mouse or a keyboard event.
// - but if there is idle time, we will instantiate them sooner.
this._register(runWhenIdle(getWindow(this._editor.getDomNode()), () => {
this._register(runWhenWindowIdle(getWindow(this._editor.getDomNode()), () => {
this._instantiateSome(EditorContributionInstantiation.BeforeFirstInteraction);
}));

// Eventually
// - these extensions will only be instantiated when there is idle time.
// - since there is no guarantee that there will ever be idle time, we set a timeout of 5s here.
this._register(runWhenIdle(getWindow(this._editor.getDomNode()), () => {
this._register(runWhenWindowIdle(getWindow(this._editor.getDomNode()), () => {
this._instantiateSome(EditorContributionInstantiation.Eventually);
}, 5000));
}
Expand Down Expand Up @@ -113,7 +112,7 @@ export class CodeEditorContributions extends Disposable {
}

public onAfterModelAttached(): void {
this._register(runWhenIdle(getWindow(this._editor?.getDomNode()), () => {
this._register(runWhenWindowIdle(getWindow(this._editor?.getDomNode()), () => {
this._instantiateSome(EditorContributionInstantiation.AfterFirstRender);
}, 50));
}
Expand Down
4 changes: 2 additions & 2 deletions src/vs/editor/common/model/textModelTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IdleDeadline, globalRunWhenIdle } from 'vs/base/common/async';
import { IdleDeadline, runWhenGlobalIdle } from 'vs/base/common/async';
import { BugIndicatingError, onUnexpectedError } from 'vs/base/common/errors';
import { setTimeout0 } from 'vs/base/common/platform';
import { StopWatch } from 'vs/base/common/stopwatch';
Expand Down Expand Up @@ -462,7 +462,7 @@ export class DefaultBackgroundTokenizer implements IBackgroundTokenizer {
}

this._isScheduled = true;
globalRunWhenIdle((deadline) => {
runWhenGlobalIdle((deadline) => {
this._isScheduled = false;

this._backgroundTokenizeWithDeadline(deadline);
Expand Down
4 changes: 2 additions & 2 deletions src/vs/editor/contrib/codelens/browser/codeLensCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { runWhenIdle } from 'vs/base/common/async';
import { Event } from 'vs/base/common/event';
import { LRUCache } from 'vs/base/common/map';
import { Range } from 'vs/editor/common/core/range';
Expand All @@ -14,6 +13,7 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from 'vs/platform/storage/common/storage';
import { $window } from 'vs/base/browser/window';
import { runWhenWindowIdle } from 'vs/base/browser/dom';

export const ICodeLensCache = createDecorator<ICodeLensCache>('ICodeLensCache');

Expand Down Expand Up @@ -53,7 +53,7 @@ export class CodeLensCache implements ICodeLensCache {

// remove old data
const oldkey = 'codelens/cache';
runWhenIdle($window, () => storageService.remove(oldkey, StorageScope.WORKSPACE));
runWhenWindowIdle($window, () => storageService.remove(oldkey, StorageScope.WORKSPACE));

// restore lens data on start
const key = 'codelens/cache2';
Expand Down
15 changes: 7 additions & 8 deletions src/vs/editor/contrib/suggest/browser/suggestController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import { alert } from 'vs/base/browser/ui/aria/aria';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IdleValue } from 'vs/base/common/async';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
Expand Down Expand Up @@ -45,7 +44,7 @@ import { ISelectedSuggestion, SuggestWidget } from './suggestWidget';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { basename, extname } from 'vs/base/common/resources';
import { hash } from 'vs/base/common/hash';
import { getWindow } from 'vs/base/browser/dom';
import { WindowIdleValue, getWindow } from 'vs/base/browser/dom';

// sticky suggest widget which doesn't disappear on focus out and such
const _sticky = false
Expand Down Expand Up @@ -112,12 +111,12 @@ export class SuggestController implements IEditorContribution {

readonly editor: ICodeEditor;
readonly model: SuggestModel;
readonly widget: IdleValue<SuggestWidget>;
readonly widget: WindowIdleValue<SuggestWidget>;

private readonly _alternatives: IdleValue<SuggestAlternatives>;
private readonly _alternatives: WindowIdleValue<SuggestAlternatives>;
private readonly _lineSuffix = new MutableDisposable<LineSuffix>();
private readonly _toDispose = new DisposableStore();
private readonly _overtypingCapturer: IdleValue<OvertypingCapturer>;
private readonly _overtypingCapturer: WindowIdleValue<OvertypingCapturer>;
private readonly _selectors = new PriorityRegistry<ISuggestItemPreselector>(s => s.priority);

private readonly _onWillInsertSuggestItem = new Emitter<{ item: CompletionItem }>();
Expand Down Expand Up @@ -146,7 +145,7 @@ export class SuggestController implements IEditorContribution {
ctxInsertMode.set(editor.getOption(EditorOption.suggest).insertMode);
this._toDispose.add(this.model.onDidTrigger(() => ctxInsertMode.set(editor.getOption(EditorOption.suggest).insertMode)));

this.widget = this._toDispose.add(new IdleValue(getWindow(editor.getDomNode()), () => {
this.widget = this._toDispose.add(new WindowIdleValue(getWindow(editor.getDomNode()), () => {

const widget = this._instantiationService.createInstance(SuggestWidget, this.editor);

Expand Down Expand Up @@ -219,11 +218,11 @@ export class SuggestController implements IEditorContribution {
}));

// Wire up text overtyping capture
this._overtypingCapturer = this._toDispose.add(new IdleValue(getWindow(editor.getDomNode()), () => {
this._overtypingCapturer = this._toDispose.add(new WindowIdleValue(getWindow(editor.getDomNode()), () => {
return this._toDispose.add(new OvertypingCapturer(this.editor, this.model));
}));

this._alternatives = this._toDispose.add(new IdleValue(getWindow(editor.getDomNode()), () => {
this._alternatives = this._toDispose.add(new WindowIdleValue(getWindow(editor.getDomNode()), () => {
return this._toDispose.add(new SuggestAlternatives(this.editor, this._contextKeyService));
}));

Expand Down
4 changes: 2 additions & 2 deletions src/vs/platform/storage/common/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Promises, RunOnceScheduler, globalRunWhenIdle } from 'vs/base/common/async';
import { Promises, RunOnceScheduler, runWhenGlobalIdle } from 'vs/base/common/async';
import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event';
import { Disposable, DisposableStore, dispose, MutableDisposable } from 'vs/base/common/lifecycle';
import { mark } from 'vs/base/common/performance';
Expand Down Expand Up @@ -343,7 +343,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
}

private doFlushWhenIdle(): void {
this.runFlushWhenIdle.value = globalRunWhenIdle(() => {
this.runFlushWhenIdle.value = runWhenGlobalIdle(() => {
if (this.shouldFlushWhenIdle()) {
this.flush();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdenti
import { Color } from 'vs/base/common/color';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { MergeGroupMode, IMergeGroupOptions, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow } from 'vs/base/browser/dom';
import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow, runWhenWindowIdle } from 'vs/base/browser/dom';
import { localize } from 'vs/nls';
import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor';
import { CloseOneEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions';
import { assertAllDefined, assertIsDefined } from 'vs/base/common/types';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { basenameOrAuthority } from 'vs/base/common/resources';
import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async';
import { RunOnceScheduler } from 'vs/base/common/async';
import { IPathService } from 'vs/workbench/services/path/common/pathService';
import { IPath, win32, posix } from 'vs/base/common/path';
import { coalesce, insert } from 'vs/base/common/arrays';
Expand Down Expand Up @@ -1618,7 +1618,7 @@ export class MultiEditorTabsControl extends EditorTabsControl {
if (this.lifecycleService.phase >= LifecyclePhase.Restored) {
scheduledLayout = scheduleAtNextAnimationFrame(getWindow(this.tabsContainer), layoutFunction);
} else {
scheduledLayout = runWhenIdle(getWindow(this.tabsContainer), layoutFunction);
scheduledLayout = runWhenWindowIdle(getWindow(this.tabsContainer), layoutFunction);
}

this.layoutScheduler.value = { options, dispose: () => scheduledLayout.dispose() };
Expand Down