diff --git a/src/vs/base/common/decorators.ts b/src/vs/base/common/decorators.ts index 17dfadbf784..be2f0a5a459 100644 --- a/src/vs/base/common/decorators.ts +++ b/src/vs/base/common/decorators.ts @@ -112,3 +112,44 @@ export function debounce(delay: number, reducer?: IDebouceReducer, initial }; }); } + +export function throttle(delay: number, reducer?: IDebouceReducer, initialValueProvider?: () => T): Function { + return createDecorator((fn, key) => { + const timerKey = `$throttle$timer$${key}`; + const resultKey = `$throttle$result$${key}`; + const lastRunKey = `$throttle$lastRun$${key}`; + const pendingKey = `$throttle$pending$${key}`; + + return function (this: any, ...args: any[]) { + if (!this[resultKey]) { + this[resultKey] = initialValueProvider ? initialValueProvider() : undefined; + } + if (this[lastRunKey] === null || this[lastRunKey] === undefined) { + this[lastRunKey] = -Number.MAX_VALUE; + } + + if (reducer) { + this[resultKey] = reducer(this[resultKey], ...args); + } + + if (this[pendingKey]) { + return; + } + + const nextTime = this[lastRunKey] + delay; + if (nextTime <= Date.now()) { + this[lastRunKey] = Date.now(); + fn.apply(this, [this[resultKey]]); + this[resultKey] = initialValueProvider ? initialValueProvider() : undefined; + } else { + this[pendingKey] = true; + this[timerKey] = setTimeout(() => { + this[pendingKey] = false; + this[lastRunKey] = Date.now(); + fn.apply(this, [this[resultKey]]); + this[resultKey] = initialValueProvider ? initialValueProvider() : undefined; + }, nextTime - Date.now()); + } + }; + }); +} diff --git a/src/vs/base/test/common/decorators.test.ts b/src/vs/base/test/common/decorators.test.ts index e7882ddb479..9a01f60a3f4 100644 --- a/src/vs/base/test/common/decorators.test.ts +++ b/src/vs/base/test/common/decorators.test.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as sinon from 'sinon'; import * as assert from 'assert'; -import { memoize, createMemoizer } from 'vs/base/common/decorators'; +import { memoize, createMemoizer, throttle } from 'vs/base/common/decorators'; suite('Decorators', () => { test('memoize should memoize methods', () => { @@ -100,7 +101,9 @@ suite('Decorators', () => { test('memoized property should not be enumerable', () => { class Foo { @memoize - get answer() { return 42; } + get answer() { + return 42; + } } const foo = new Foo(); @@ -112,7 +115,9 @@ suite('Decorators', () => { test('memoized property should not be writable', () => { class Foo { @memoize - get answer() { return 42; } + get answer() { + return 42; + } } const foo = new Foo(); @@ -131,7 +136,9 @@ suite('Decorators', () => { let counter = 0; class Foo { @memoizer - get answer() { return ++counter; } + get answer() { + return ++counter; + } } const foo = new Foo(); @@ -145,4 +152,49 @@ suite('Decorators', () => { assert.equal(foo.answer, 3); assert.equal(foo.answer, 3); }); + + test('throttle', () => { + const spy = sinon.spy(); + const clock = sinon.useFakeTimers(); + try { + class ThrottleTest { + private _handle: Function; + + constructor(fn: Function) { + this._handle = fn; + } + + @throttle( + 100, + (a: number, b: number) => a + b, + () => 0 + ) + report(p: number): void { + this._handle(p); + } + } + + const t = new ThrottleTest(spy); + + t.report(1); + t.report(2); + t.report(3); + assert.deepEqual(spy.args, [[1]]); + + clock.tick(200); + assert.deepEqual(spy.args, [[1], [5]]); + spy.reset(); + + t.report(4); + t.report(5); + clock.tick(50); + t.report(6); + + assert.deepEqual(spy.args, [[4]]); + clock.tick(60); + assert.deepEqual(spy.args, [[4], [11]]); + } finally { + clock.restore(); + } + }); }); diff --git a/src/vs/workbench/api/common/extHostProgress.ts b/src/vs/workbench/api/common/extHostProgress.ts index afb0fb647f8..27a4e145c42 100644 --- a/src/vs/workbench/api/common/extHostProgress.ts +++ b/src/vs/workbench/api/common/extHostProgress.ts @@ -9,7 +9,7 @@ import { ProgressLocation } from './extHostTypeConverters'; import { Progress, IProgressStep } from 'vs/platform/progress/common/progress'; import { localize } from 'vs/nls'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; -import { debounce } from 'vs/base/common/decorators'; +import { throttle } from 'vs/base/common/decorators'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export class ExtHostProgress implements ExtHostProgressShape { @@ -85,7 +85,7 @@ class ProgressCallback extends Progress { super(p => this.throttledReport(p)); } - @debounce(100, (result: IProgressStep, currentValue: IProgressStep) => mergeProgress(result, currentValue), () => Object.create(null)) + @throttle(100, (result: IProgressStep, currentValue: IProgressStep) => mergeProgress(result, currentValue), () => Object.create(null)) throttledReport(p: IProgressStep): void { this._proxy.$progressReport(this._handle, p); }