From b4330d43c32e7d306f4d250243974595261753a3 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Mon, 13 Apr 2020 15:34:38 -0400 Subject: [PATCH 1/4] Add an async lazy class and lazy lifetimes --- src/tasks/node/NodeTaskHelper.ts | 2 +- src/utils/lazy.ts | 57 +++++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/tasks/node/NodeTaskHelper.ts b/src/tasks/node/NodeTaskHelper.ts index 9ce1c9787f..47ccb05b6c 100644 --- a/src/tasks/node/NodeTaskHelper.ts +++ b/src/tasks/node/NodeTaskHelper.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import { WorkspaceFolder } from 'vscode'; -import Lazy from '../../utils/lazy'; +import { Lazy } from '../../utils/lazy'; import { inferCommand, inferPackageName, InspectMode, NodePackage, readPackage } from '../../utils/nodeUtils'; import { resolveVariables, unresolveWorkspaceFolder } from '../../utils/resolveVariables'; import { DockerBuildOptions, DockerBuildTaskDefinitionBase } from '../DockerBuildTaskDefinitionBase'; diff --git a/src/utils/lazy.ts b/src/utils/lazy.ts index c57960d2a2..11af733f02 100644 --- a/src/utils/lazy.ts +++ b/src/utils/lazy.ts @@ -7,7 +7,7 @@ export class Lazy { private _isValueCreated: boolean = false; private _value: T | undefined; - public constructor(private readonly valueFactory: () => T) { + public constructor(private readonly valueFactory: () => T, private readonly _valueLifetime?: number) { } public get isValueCreated(): boolean { @@ -15,13 +15,60 @@ export class Lazy { } public get value(): T { - if (!this._isValueCreated) { - this._value = this.valueFactory(); - this._isValueCreated = true; + if (this._isValueCreated) { + return this._value; + } + + this._value = this.valueFactory(); + this._isValueCreated = true; + + if (this._valueLifetime) { + const reset = setTimeout(() => { + this._isValueCreated = false; + this._value = undefined; + clearTimeout(reset); + }, this._valueLifetime); } return this._value; } } -export default Lazy; +export class AsyncLazy { + private _isValueCreated: boolean = false; + private _value: T | undefined; + private _valuePromise: Promise | undefined; + + public constructor(private readonly valueFactory: () => Promise, private readonly _valueLifetime?: number) { + } + + public async getValue(): Promise { + if (this._isValueCreated) { + return this._value; + } + + this._isValueCreated = false; + + const isPrimaryPromise = this._valuePromise === undefined; // The first caller is "primary" + const fnLocalPromiseRef = this._valuePromise = this._valuePromise ?? this.valueFactory(); // Await any currently-running Promise if there is one + this._value = await fnLocalPromiseRef; + this._valuePromise = undefined; + + this._isValueCreated = true; + + if (this._valueLifetime && isPrimaryPromise) { + const reset = setTimeout(() => { + // Will only clear out values if there isn't a currently-running Promise + // If there is, this timer will skip, but when that Promise finishes it will go through this code and register a new timer + if (this._valuePromise === undefined) { + this._isValueCreated = false; + this._value = undefined; + } + + clearTimeout(reset); + }, this._valueLifetime); + } + + return this._value; + } +} From 9996fd48c3a1de2950ed129be7df69620f51b43e Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 15 Apr 2020 09:17:30 -0400 Subject: [PATCH 2/4] PR feedback --- src/utils/lazy.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/utils/lazy.ts b/src/utils/lazy.ts index 11af733f02..c48a3cac38 100644 --- a/src/utils/lazy.ts +++ b/src/utils/lazy.ts @@ -42,19 +42,28 @@ export class AsyncLazy { public constructor(private readonly valueFactory: () => Promise, private readonly _valueLifetime?: number) { } + public get isValueCreated(): boolean { + return this._isValueCreated; + } + public async getValue(): Promise { if (this._isValueCreated) { return this._value; } - this._isValueCreated = false; - const isPrimaryPromise = this._valuePromise === undefined; // The first caller is "primary" - const fnLocalPromiseRef = this._valuePromise = this._valuePromise ?? this.valueFactory(); // Await any currently-running Promise if there is one - this._value = await fnLocalPromiseRef; - this._valuePromise = undefined; - this._isValueCreated = true; + if (isPrimaryPromise) { + this._valuePromise = this.valueFactory(); + } + + const result = await this._valuePromise; + + if (isPrimaryPromise) { + this._value = result; + this._valuePromise = undefined; + this._isValueCreated = true; + } if (this._valueLifetime && isPrimaryPromise) { const reset = setTimeout(() => { @@ -69,6 +78,6 @@ export class AsyncLazy { }, this._valueLifetime); } - return this._value; + return result; } } From 183340e8d990de50ac8065ce6ca8927bf69fe6ad Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 15 Apr 2020 10:10:40 -0400 Subject: [PATCH 3/4] Add unit tests --- extension.bundle.ts | 2 + test/utils/lazy.test.ts | 117 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 test/utils/lazy.test.ts diff --git a/extension.bundle.ts b/extension.bundle.ts index bb93c227a9..9dd8f87d91 100644 --- a/extension.bundle.ts +++ b/extension.bundle.ts @@ -28,6 +28,8 @@ export { DotNetClient } from './src/debugging/coreclr/CommandLineDotNetClient'; export { compareBuildImageOptions, LaunchOptions } from './src/debugging/coreclr/dockerManager'; export { FileSystemProvider } from './src/debugging/coreclr/fsProvider'; export { LineSplitter } from './src/debugging/coreclr/lineSplitter'; +export { delay } from './src/utils/delay'; +export { Lazy, AsyncLazy } from './src/utils/lazy'; export { OSProvider } from './src/utils/LocalOSProvider'; export { DockerDaemonIsLinuxPrerequisite, DockerfileExistsPrerequisite, DotNetSdkInstalledPrerequisite, LinuxUserInDockerGroupPrerequisite, MacNuGetFallbackFolderSharedPrerequisite } from './src/debugging/coreclr/prereqManager'; export { ext } from './src/extensionVariables'; diff --git a/test/utils/lazy.test.ts b/test/utils/lazy.test.ts new file mode 100644 index 0000000000..9c8853ddb9 --- /dev/null +++ b/test/utils/lazy.test.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Lazy, AsyncLazy } from "../../extension.bundle"; +import { delay } from '../../src/utils/delay'; + +suite('(unit) Lazy tests', () => { + suite('Lazy', () => { + test('Normal', async () => { + let factoryCallCount = 0; + const lazy: Lazy = new Lazy(() => { + factoryCallCount++; + return true; + }); + + lazy.value; + lazy.value; + + assert.equal(factoryCallCount, 1, 'Incorrect number of value factory calls.'); + }); + + test('With lifetime', async () => { + let factoryCallCount = 0; + const lazy: Lazy = new Lazy(() => { + factoryCallCount++; + return true; + }, 5); + + lazy.value; + lazy.value; + + assert.equal(factoryCallCount, 1, 'Incorrect number of value factory calls.'); + + await delay(10); + lazy.value; + lazy.value; + + assert.equal(factoryCallCount, 2, 'Incorrect number of value factory calls.'); + }); + }); + + suite('AsyncLazy', () => { + test('Normal', async () => { + let factoryCallCount = 0; + const lazy: AsyncLazy = new AsyncLazy(async () => { + factoryCallCount++; + await delay(5); + return true; + }); + + await lazy.getValue(); + await lazy.getValue(); + + assert.equal(factoryCallCount, 1, 'Incorrect number of value factory calls.'); + }); + + test('Simultaneous callers', async () => { + let factoryCallCount = 0; + const lazy: AsyncLazy = new AsyncLazy(async () => { + factoryCallCount++; + await delay(5); + return true; + }); + + const p1 = lazy.getValue(); + const p2 = lazy.getValue(); + await Promise.all([p1, p2]); + + assert.equal(factoryCallCount, 1, 'Incorrect number of value factory calls.'); + }); + + test('With lifetime', async () => { + let factoryCallCount = 0; + const lazy: AsyncLazy = new AsyncLazy(async () => { + factoryCallCount++; + await delay(5); + return true; + }, 10); + + await lazy.getValue(); + await lazy.getValue(); + + assert.equal(factoryCallCount, 1, 'Incorrect number of value factory calls.'); + + await delay(15); + await lazy.getValue(); + await lazy.getValue(); + + assert.equal(factoryCallCount, 2, 'Incorrect number of value factory calls.'); + }); + + test('Simultaneous callers with lifetime', async () => { + let factoryCallCount = 0; + const lazy: AsyncLazy = new AsyncLazy(async () => { + factoryCallCount++; + await delay(5); + return true; + }, 10); + + const p1 = lazy.getValue(); + const p2 = lazy.getValue(); + await Promise.all([p1, p2]); + + assert.equal(factoryCallCount, 1, 'Incorrect number of value factory calls.'); + + await delay(15); + const p3 = lazy.getValue(); + const p4 = lazy.getValue(); + await Promise.all([p3, p4]); + + assert.equal(factoryCallCount, 2, 'Incorrect number of value factory calls.'); + }); + }); +}); From d3b2be4b0c31b05e7c2b492e4670d3709db81032 Mon Sep 17 00:00:00 2001 From: "Brandon Waterloo [MSFT]" <36966225+bwateratmsft@users.noreply.github.com> Date: Wed, 15 Apr 2020 11:43:15 -0400 Subject: [PATCH 4/4] Fix UT failure --- test/utils/lazy.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/lazy.test.ts b/test/utils/lazy.test.ts index 9c8853ddb9..39f107f27e 100644 --- a/test/utils/lazy.test.ts +++ b/test/utils/lazy.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { Lazy, AsyncLazy } from "../../extension.bundle"; -import { delay } from '../../src/utils/delay'; +import { delay } from '../../extension.bundle'; suite('(unit) Lazy tests', () => { suite('Lazy', () => {