Skip to content

Commit

Permalink
move LanguageFeatureRequestDelays into `ILanguageFeatureDebounceSer…
Browse files Browse the repository at this point in the history
…vice`, have better defaults, and adopt for code lens and inlay hints, #140557
  • Loading branch information
jrieken committed Jan 13, 2022
1 parent c393048 commit 0cfa366
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 81 deletions.
7 changes: 4 additions & 3 deletions src/vs/base/common/numbers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export class MovingAverage {
private _n = 1;
private _val = 0;

update(value: number): this {
update(value: number): number {
this._val = this._val + (value - this._val) / this._n;
this._n += 1;
return this;
return this._val;
}

get value(): number {
Expand All @@ -49,7 +49,7 @@ export class SlidingWindowAverage {
this._values.fill(0, 0, size);
}

update(value: number) {
update(value: number): number {
const oldValue = this._values[this._index];
this._values[this._index] = value;
this._index = (this._index + 1) % this._values.length;
Expand All @@ -62,6 +62,7 @@ export class SlidingWindowAverage {
}

this._val = this._sum / this._n;
return this._val;
}

get value(): number {
Expand Down
61 changes: 0 additions & 61 deletions src/vs/editor/common/languages/languageFeatureRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { Emitter, Event } from 'vs/base/common/event';
import { doHash } from 'vs/base/common/hash';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { LRUCache } from 'vs/base/common/map';
import { SlidingWindowAverage } from 'vs/base/common/numbers';
import { ITextModel } from 'vs/editor/common/model';
import { LanguageFilter, LanguageSelector, score } from 'vs/editor/common/languages/languageSelector';
import { shouldSynchronizeModel } from 'vs/editor/common/services/model';
Expand Down Expand Up @@ -177,61 +174,3 @@ export class LanguageFeatureRegistry<T> {
}
}
}


const _hashes = new WeakMap<object, number>();
let pool = 0;
function weakHash(obj: object): number {
let value = _hashes.get(obj);
if (value === undefined) {
value = ++pool;
_hashes.set(obj, value);
}
return value;
}


/**
* Keeps moving average per model and set of providers so that requests
* can be debounce according to the provider performance
*/
export class LanguageFeatureRequestDelays {

private readonly _cache = new LRUCache<string, SlidingWindowAverage>(50, 0.7);


constructor(
private readonly _registry: LanguageFeatureRegistry<object>,
readonly min: number,
readonly max: number = Number.MAX_SAFE_INTEGER,
) { }

private _key(model: ITextModel): string {
return model.id + this._registry.all(model).reduce((hashVal, obj) => doHash(weakHash(obj), hashVal), 0);
}

private _clamp(value: number | undefined): number {
if (value === undefined) {
return this.min;
} else {
return Math.min(this.max, Math.max(this.min, Math.floor(value * 1.3)));
}
}

get(model: ITextModel): number {
const key = this._key(model);
const avg = this._cache.get(key);
return this._clamp(avg?.value);
}

update(model: ITextModel, value: number): number {
const key = this._key(model);
let avg = this._cache.get(key);
if (!avg) {
avg = new SlidingWindowAverage(12);
this._cache.set(key, avg);
}
avg.update(value);
return this.get(model);
}
}
122 changes: 122 additions & 0 deletions src/vs/editor/common/services/languageFeatureDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { doHash } from 'vs/base/common/hash';
import { LRUCache } from 'vs/base/common/map';
import { clamp, MovingAverage, SlidingWindowAverage } from 'vs/base/common/numbers';
import { LanguageFeatureRegistry } from 'vs/editor/common/languages/languageFeatureRegistry';
import { ITextModel } from 'vs/editor/common/model';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';


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

export interface ILanguageFeatureDebounceService {

readonly _serviceBrand: undefined;

for(feature: LanguageFeatureRegistry<object>, config?: { min?: number, salt?: string }): IFeatureDebounceInformation;
}

export interface IFeatureDebounceInformation {
get(model: ITextModel): number;
update(model: ITextModel, value: number): number;
default(): number;
}

namespace IdentityHash {
const _hashes = new WeakMap<object, number>();
let pool = 0;
export function of(obj: object): number {
let value = _hashes.get(obj);
if (value === undefined) {
value = ++pool;
_hashes.set(obj, value);
}
return value;
}
}

export class FeatureDebounceInformation implements IFeatureDebounceInformation {

private readonly _cache = new LRUCache<string, SlidingWindowAverage>(50, 0.7);

constructor(
private readonly _registry: LanguageFeatureRegistry<object>,
private readonly _default: () => number,
private readonly _min: number,
private readonly _max: number = Number.MAX_SAFE_INTEGER,
) { }

private _key(model: ITextModel): string {
return model.id + this._registry.all(model).reduce((hashVal, obj) => doHash(IdentityHash.of(obj), hashVal), 0);
}

get(model: ITextModel): number {
const key = this._key(model);
const avg = this._cache.get(key);
return avg
? clamp(avg.value, this._min, this._max)
: this.default();
}

update(model: ITextModel, value: number): number {
const key = this._key(model);
let avg = this._cache.get(key);
if (!avg) {
avg = new SlidingWindowAverage(12);
this._cache.set(key, avg);
}
return clamp(avg.update(value), this._min, this._max);
}

private _overall(): number {
const result = new MovingAverage();
for (const [, avg] of this._cache) {
result.update(avg.value);
}
return result.value;
}

default() {
const value = (this._overall() | 0) || this._default();
return clamp(value, this._min, this._max);
}
}


export class LanguageFeatureDebounceService implements ILanguageFeatureDebounceService {

declare _serviceBrand: undefined;

private readonly _data = new Map<string, FeatureDebounceInformation>();

for(feature: LanguageFeatureRegistry<object>, config?: { min?: number, key?: string }): IFeatureDebounceInformation {
const min = config?.min ?? 50;
const extra = config?.key ?? undefined;
const key = `${IdentityHash.of(feature)},${min}${extra ? ',' + extra : ''}`;
let info = this._data.get(key);
if (!info) {
info = new FeatureDebounceInformation(feature,
() => (this._overallAverage() | 0) || (min * 1.5), // default is overall default or derived from min-value
min
);
this._data.set(key, info);
}
return info;
}

private _overallAverage(): number {
// Average of all language features. Not a great value but an approximation
let result = new MovingAverage();
for (let info of this._data.values()) {
result.update(info.default());
}
return result.value;
}
}

registerSingleton(ILanguageFeatureDebounceService, LanguageFeatureDebounceService, true);
19 changes: 12 additions & 7 deletions src/vs/editor/contrib/codelens/codelensController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { IModelDecorationsChangeAccessor } from 'vs/editor/common/model';
import { CodeLens, CodeLensProviderRegistry, Command } from 'vs/editor/common/languages';
import { LanguageFeatureRequestDelays } from 'vs/editor/common/languages/languageFeatureRegistry';
import { CodeLensItem, CodeLensModel, getCodeLensModel } from 'vs/editor/contrib/codelens/codelens';
import { ICodeLensCache } from 'vs/editor/contrib/codelens/codeLensCache';
import { CodeLensHelper, CodeLensWidget } from 'vs/editor/contrib/codelens/codelensWidget';
import { localize } from 'vs/nls';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce';

export class CodeLensContribution implements IEditorContribution {

Expand All @@ -35,20 +35,25 @@ export class CodeLensContribution implements IEditorContribution {
private readonly _styleClassName: string;
private readonly _lenses: CodeLensWidget[] = [];

private readonly _getCodeLensModelDelays = new LanguageFeatureRequestDelays(CodeLensProviderRegistry, 250, 2500);
private readonly _provideCodeLensDebounce: IFeatureDebounceInformation;
private readonly _resolveCodeLensesDebounce: IFeatureDebounceInformation;
private readonly _resolveCodeLensesScheduler: RunOnceScheduler;

private _getCodeLensModelPromise: CancelablePromise<CodeLensModel> | undefined;
private _oldCodeLensModels = new DisposableStore();
private _currentCodeLensModel: CodeLensModel | undefined;
private readonly _resolveCodeLensesDelays = new LanguageFeatureRequestDelays(CodeLensProviderRegistry, 250, 2500);
private readonly _resolveCodeLensesScheduler = new RunOnceScheduler(() => this._resolveCodeLensesInViewport(), this._resolveCodeLensesDelays.min);
private _resolveCodeLensesPromise: CancelablePromise<any> | undefined;

constructor(
private readonly _editor: ICodeEditor,
@ILanguageFeatureDebounceService debounceService: ILanguageFeatureDebounceService,
@ICommandService private readonly _commandService: ICommandService,
@INotificationService private readonly _notificationService: INotificationService,
@ICodeLensCache private readonly _codeLensCache: ICodeLensCache
) {
this._provideCodeLensDebounce = debounceService.for(CodeLensProviderRegistry, { min: 250 });
this._resolveCodeLensesDebounce = debounceService.for(CodeLensProviderRegistry, { min: 250, salt: 'resolve' });
this._resolveCodeLensesScheduler = new RunOnceScheduler(() => this._resolveCodeLensesInViewport(), this._resolveCodeLensesDebounce.default());

this._disposables.add(this._editor.onDidChangeModel(() => this._onModelChange()));
this._disposables.add(this._editor.onDidChangeModelLanguage(() => this._onModelChange()));
Expand Down Expand Up @@ -186,7 +191,7 @@ export class CodeLensContribution implements IEditorContribution {
this._codeLensCache.put(model, result);

// update moving average
const newDelay = this._getCodeLensModelDelays.update(model, Date.now() - t1);
const newDelay = this._provideCodeLensDebounce.update(model, Date.now() - t1);
scheduler.delay = newDelay;

// render lenses
Expand All @@ -195,7 +200,7 @@ export class CodeLensContribution implements IEditorContribution {
this._resolveCodeLensesInViewportSoon();
}, onUnexpectedError);

}, this._getCodeLensModelDelays.get(model));
}, this._provideCodeLensDebounce.get(model));

this._localToDispose.add(scheduler);
this._localToDispose.add(toDisposable(() => this._resolveCodeLensesScheduler.cancel()));
Expand Down Expand Up @@ -421,7 +426,7 @@ export class CodeLensContribution implements IEditorContribution {
this._resolveCodeLensesPromise.then(() => {

// update moving average
const newDelay = this._resolveCodeLensesDelays.update(model, Date.now() - t1);
const newDelay = this._resolveCodeLensesDebounce.update(model, Date.now() - t1);
this._resolveCodeLensesScheduler.delay = newDelay;

if (this._currentCodeLensModel) { // update the cached state with new resolved items
Expand Down
8 changes: 4 additions & 4 deletions src/vs/editor/contrib/documentSymbols/outlineModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { IPosition, Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
import { DocumentSymbol, DocumentSymbolProvider, DocumentSymbolProviderRegistry } from 'vs/editor/common/languages';
import { LanguageFeatureRequestDelays } from 'vs/editor/common/languages/languageFeatureRegistry';
import { MarkerSeverity } from 'vs/platform/markers/common/markers';
import { FeatureDebounceInformation } from 'vs/editor/common/services/languageFeatureDebounce';

export abstract class TreeElement {

Expand Down Expand Up @@ -206,7 +206,7 @@ export class OutlineGroup extends TreeElement {

export class OutlineModel extends TreeElement {

private static readonly _requestDurations = new LanguageFeatureRequestDelays(DocumentSymbolProviderRegistry, 350);
private static readonly _requestDurations = new FeatureDebounceInformation(DocumentSymbolProviderRegistry, () => 350, 350); // todo@jrieken ADOPT debounce service
private static readonly _requests = new LRUCache<string, { promiseCnt: number, source: CancellationTokenSource, promise: Promise<any>, model: OutlineModel | undefined }>(9, 0.75);
private static readonly _keys = new class {

Expand Down Expand Up @@ -281,8 +281,8 @@ export class OutlineModel extends TreeElement {
});
}

static getRequestDelay(textModel: ITextModel | null): number {
return textModel ? this._requestDurations.get(textModel) : this._requestDurations.min;
static getRequestDelay(textModel: ITextModel): number {
return this._requestDurations.get(textModel);
}

private static _create(textModel: ITextModel, token: CancellationToken): Promise<OutlineModel> {
Expand Down
10 changes: 6 additions & 4 deletions src/vs/editor/contrib/inlayHints/inlayHintsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import { EditorOption, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/edit
import { Range } from 'vs/editor/common/core/range';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import * as languages from 'vs/editor/common/languages';
import { LanguageFeatureRequestDelays } from 'vs/editor/common/languages/languageFeatureRegistry';
import { IModelDeltaDecoration, InjectedTextCursorStops, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model';
import { ModelDecorationInjectedTextOptions } from 'vs/editor/common/model/textModel';
import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { ClickLinkGesture, ClickLinkMouseEvent } from 'vs/editor/contrib/gotoSymbol/link/clickLinkGesture';
import { InlayHintAnchor, InlayHintItem, InlayHintsFragments } from 'vs/editor/contrib/inlayHints/inlayHints';
Expand Down Expand Up @@ -74,7 +74,7 @@ export class InlayHintsController implements IEditorContribution {

private readonly _disposables = new DisposableStore();
private readonly _sessionDisposables = new DisposableStore();
private readonly _getInlayHintsDelays = new LanguageFeatureRequestDelays(languages.InlayHintsProviderRegistry, 25, 500);
private readonly _debounceInfo: IFeatureDebounceInformation;
private readonly _cache = new InlayHintsCache();
private readonly _decorationsMetadata = new Map<string, { item: InlayHintItem, classNameRef: IDisposable; }>();
private readonly _ruleFactory = new DynamicCssRules(this._editor);
Expand All @@ -83,10 +83,12 @@ export class InlayHintsController implements IEditorContribution {

constructor(
private readonly _editor: ICodeEditor,
@ILanguageFeatureDebounceService _featureDebounce: ILanguageFeatureDebounceService,
@ICommandService private readonly _commandService: ICommandService,
@INotificationService private readonly _notificationService: INotificationService,
@IInstantiationService private readonly _instaService: IInstantiationService,
) {
this._debounceInfo = _featureDebounce.for(languages.InlayHintsProviderRegistry, { min: 25 });
this._disposables.add(languages.InlayHintsProviderRegistry.onDidChange(() => this._update()));
this._disposables.add(_editor.onDidChangeModel(() => this._update()));
this._disposables.add(_editor.onDidChangeModelLanguage(() => this._update()));
Expand Down Expand Up @@ -133,7 +135,7 @@ export class InlayHintsController implements IEditorContribution {
cts = new CancellationTokenSource();

const inlayHints = await InlayHintsFragments.create(model, this._getHintsRanges(), cts.token);
scheduler.delay = this._getInlayHintsDelays.update(model, Date.now() - t1);
scheduler.delay = this._debounceInfo.update(model, Date.now() - t1);
if (cts.token.isCancellationRequested) {
inlayHints.dispose();
return;
Expand All @@ -150,7 +152,7 @@ export class InlayHintsController implements IEditorContribution {
this._updateHintsDecorators(inlayHints.ranges, inlayHints.items);
this._cacheHintsForFastRestore(model);

}, this._getInlayHintsDelays.get(model));
}, this._debounceInfo.get(model));

this._sessionDisposables.add(scheduler);
this._sessionDisposables.add(toDisposable(() => cts?.dispose(true)));
Expand Down
2 changes: 2 additions & 0 deletions src/vs/editor/test/browser/testCodeEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService';
import { EditorConfiguration } from 'vs/editor/browser/config/editorConfiguration';
import { ILanguageFeatureDebounceService, LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce';

export interface ITestCodeEditor extends IActiveCodeEditor {
getViewModel(): ViewModel | undefined;
Expand Down Expand Up @@ -171,6 +172,7 @@ export function createCodeEditorServices(disposables: DisposableStore, services:
define(IContextKeyService, MockContextKeyService);
define(ICommandService, TestCommandService);
define(ITelemetryService, NullTelemetryServiceShape);
define(ILanguageFeatureDebounceService, LanguageFeatureDebounceService);

const instantiationService = new TestInstantiationService(services);
disposables.add(toDisposable(() => {
Expand Down
Loading

0 comments on commit 0cfa366

Please sign in to comment.