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

Perfs #15530

Closed
wants to merge 7 commits into from
Closed

Perfs #15530

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/notebooks/controllers/ipywidgets/rendererVersionChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { IDisposable, IDisposableRegistry } from '../../../platform/common/types
import { isJupyterNotebook } from '../../../platform/common/utils';
import { Common, DataScience } from '../../../platform/common/utils/localize';
import { noop } from '../../../platform/common/utils/misc';
import { Delayer } from '../../../platform/common/utils/async';

@injectable()
export class RendererVersionChecker implements IExtensionSyncActivationService {
Expand All @@ -22,7 +23,15 @@ export class RendererVersionChecker implements IExtensionSyncActivationService {
dispose(this.disposables);
}
activate(): void {
workspace.onDidChangeNotebookDocument(this.onDidChangeNotebookDocument, this, this.disposables);
const delayer = new Delayer<void>(1_000);
this.disposables.push(delayer);
workspace.onDidChangeNotebookDocument(
(e) => {
void delayer.trigger(() => this.onDidChangeNotebookDocument(e));
},
this,
this.disposables
);
}
private onDidChangeNotebookDocument(e: NotebookDocumentChangeEvent) {
if (!isJupyterNotebook(e.notebook)) {
Expand Down
18 changes: 14 additions & 4 deletions src/notebooks/outputs/jupyterCellOutputMimeTypeTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import { JupyterNotebookView } from '../../platform/common/constants';
import { dispose } from '../../platform/common/utils/lifecycle';
import { IDisposable, IDisposableRegistry } from '../../platform/common/types';
import { isJupyterNotebook } from '../../platform/common/utils';
import { ResourceTypeTelemetryProperty, sendTelemetryEvent, Telemetry } from '../../telemetry';
import {
onDidChangeTelemetryEnablement,
ResourceTypeTelemetryProperty,
sendTelemetryEvent,
Telemetry
} from '../../telemetry';
import { isTelemetryDisabled } from '../../telemetry';

/**
Expand All @@ -25,14 +30,13 @@ import { isTelemetryDisabled } from '../../telemetry';
export class CellOutputMimeTypeTracker implements IExtensionSyncActivationService, IDisposable {
private sentMimeTypes: Set<string> = new Set<string>();
private readonly disposables: IDisposable[] = [];
private get isTelemetryDisabled() {
return isTelemetryDisabled();
}
private isTelemetryDisabled: boolean;

constructor(@inject(IDisposableRegistry) disposables: IDisposableRegistry) {
disposables.push(this);
}
public activate() {
this.isTelemetryDisabled = isTelemetryDisabled();
workspace.onDidOpenNotebookDocument(this.onDidOpenCloseDocument, this, this.disposables);
workspace.onDidCloseNotebookDocument(this.onDidOpenCloseDocument, this, this.disposables);
workspace.onDidSaveNotebookDocument(this.onDidOpenCloseDocument, this, this.disposables);
Expand All @@ -41,6 +45,12 @@ export class CellOutputMimeTypeTracker implements IExtensionSyncActivationServic
this,
this.disposables
);
this.disposables.push(
onDidChangeTelemetryEnablement((enabled) => {
this.isTelemetryDisabled = enabled;
}),
this
);
}

public dispose() {
Expand Down
263 changes: 263 additions & 0 deletions src/platform/common/utils/async.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { CancellationError } from 'vscode';
import type { IDisposable } from '../types';
import { MicrotaskDelay } from './symbols';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type PromiseFunction = (...any: any[]) => Promise<any>;

Expand Down Expand Up @@ -181,3 +185,262 @@ export class PromiseChain {
return deferred.promise;
}
}


export interface ITask<T> {
(): T;
}

/**
* A helper to prevent accumulation of sequential async tasks.
*
* Imagine a mail man with the sole task of delivering letters. As soon as
* a letter submitted for delivery, he drives to the destination, delivers it
* and returns to his base. Imagine that during the trip, N more letters were submitted.
* When the mail man returns, he picks those N letters and delivers them all in a
* single trip. Even though N+1 submissions occurred, only 2 deliveries were made.
*
* The throttler implements this via the queue() method, by providing it a task
* factory. Following the example:
*
* const throttler = new Throttler();
* const letters = [];
*
* function deliver() {
* const lettersToDeliver = letters;
* letters = [];
* return makeTheTrip(lettersToDeliver);
* }
*
* function onLetterReceived(l) {
* letters.push(l);
* throttler.queue(deliver);
* }
*/
export class Throttler implements IDisposable {

private activePromise: Promise<any> | null;
private queuedPromise: Promise<any> | null;
private queuedPromiseFactory: ITask<Promise<any>> | null;

private isDisposed = false;

constructor() {
this.activePromise = null;
this.queuedPromise = null;
this.queuedPromiseFactory = null;
}

queue<T>(promiseFactory: ITask<Promise<T>>): Promise<T> {
if (this.isDisposed) {
return Promise.reject(new Error('Throttler is disposed'));
}

if (this.activePromise) {
this.queuedPromiseFactory = promiseFactory;

if (!this.queuedPromise) {
const onComplete = () => {
this.queuedPromise = null;

if (this.isDisposed) {
return;
}

const result = this.queue(this.queuedPromiseFactory!);
this.queuedPromiseFactory = null;

return result;
};

this.queuedPromise = new Promise(resolve => {
this.activePromise!.then(onComplete, onComplete).then(resolve);
});
}

return new Promise((resolve, reject) => {
this.queuedPromise!.then(resolve, reject);
});
}

this.activePromise = promiseFactory();

return new Promise((resolve, reject) => {
this.activePromise!.then((result: T) => {
this.activePromise = null;
resolve(result);
}, (err: unknown) => {
this.activePromise = null;
reject(err);
});
});
}

dispose(): void {
this.isDisposed = true;
}
}


interface IScheduledLater extends IDisposable {
isTriggered(): boolean;
}

const timeoutDeferred = (timeout: number, fn: () => void): IScheduledLater => {
let scheduled = true;
const handle = setTimeout(() => {
scheduled = false;
fn();
}, timeout);
return {
isTriggered: () => scheduled,
dispose: () => {
clearTimeout(handle);
scheduled = false;
}
};
};

const microtaskDeferred = (fn: () => void): IScheduledLater => {
let scheduled = true;
queueMicrotask(() => {
if (scheduled) {
scheduled = false;
fn();
}
});

return {
isTriggered: () => scheduled,
dispose: () => {
scheduled = false;
}
};
};

/**
* A helper to delay (debounce) execution of a task that is being requested often.
*
* Following the throttler, now imagine the mail man wants to optimize the number of
* trips proactively. The trip itself can be long, so he decides not to make the trip
* as soon as a letter is submitted. Instead he waits a while, in case more
* letters are submitted. After said waiting period, if no letters were submitted, he
* decides to make the trip. Imagine that N more letters were submitted after the first
* one, all within a short period of time between each other. Even though N+1
* submissions occurred, only 1 delivery was made.
*
* The delayer offers this behavior via the trigger() method, into which both the task
* to be executed and the waiting period (delay) must be passed in as arguments. Following
* the example:
*
* const delayer = new Delayer(WAITING_PERIOD);
* const letters = [];
*
* function letterReceived(l) {
* letters.push(l);
* delayer.trigger(() => { return makeTheTrip(); });
* }
*/
export class Delayer<T> implements IDisposable {
private deferred: IScheduledLater | null;
private completionPromise: Promise<any> | null;
private doResolve: ((value?: any | Promise<any>) => void) | null;
private doReject: ((err: any) => void) | null;
private task: ITask<T | Promise<T>> | null;

constructor(public defaultDelay: number | typeof MicrotaskDelay) {
this.deferred = null;
this.completionPromise = null;
this.doResolve = null;
this.doReject = null;
this.task = null;
}

trigger(task: ITask<T | Promise<T>>, delay = this.defaultDelay): Promise<T> {
this.task = task;
this.cancelTimeout();

if (!this.completionPromise) {
this.completionPromise = new Promise((resolve, reject) => {
this.doResolve = resolve;
this.doReject = reject;
}).then(() => {
this.completionPromise = null;
this.doResolve = null;
if (this.task) {
const task = this.task;
this.task = null;
return task();
}
return undefined;
});
}

const fn = () => {
this.deferred = null;
this.doResolve?.(null);
};

this.deferred = delay === MicrotaskDelay ? microtaskDeferred(fn) : timeoutDeferred(delay, fn);

return this.completionPromise;
}

isTriggered(): boolean {
return !!this.deferred?.isTriggered();
}

cancel(): void {
this.cancelTimeout();

if (this.completionPromise) {
this.doReject?.(new CancellationError());
this.completionPromise = null;
}
}

private cancelTimeout(): void {
this.deferred?.dispose();
this.deferred = null;
}

dispose(): void {
this.cancel();
}
}

/**
* A helper to delay execution of a task that is being requested often, while
* preventing accumulation of consecutive executions, while the task runs.
*
* The mail man is clever and waits for a certain amount of time, before going
* out to deliver letters. While the mail man is going out, more letters arrive
* and can only be delivered once he is back. Once he is back the mail man will
* do one more trip to deliver the letters that have accumulated while he was out.
*/
export class ThrottledDelayer<T> {
private delayer: Delayer<Promise<T>>;
private throttler: Throttler;

constructor(defaultDelay: number) {
this.delayer = new Delayer(defaultDelay);
this.throttler = new Throttler();
}

trigger(promiseFactory: ITask<Promise<T>>, delay?: number): Promise<T> {
return this.delayer.trigger(() => this.throttler.queue(promiseFactory), delay) as unknown as Promise<T>;
}

isTriggered(): boolean {
return this.delayer.isTriggered();
}

cancel(): void {
this.delayer.cancel();
}

dispose(): void {
this.delayer.dispose();
this.throttler.dispose();
}
}
9 changes: 9 additions & 0 deletions src/platform/common/utils/symbols.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

/**
* Can be passed into the Delayed to defer using a microtask
* */
export const MicrotaskDelay = Symbol('MicrotaskDelay');
13 changes: 12 additions & 1 deletion src/platform/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { StopWatch } from '../common/utils/stopWatch';
import { ExcludeType, noop, PickType, UnionToIntersection } from '../common/utils/misc';
import { populateTelemetryWithErrorInfo } from '../errors';
import { TelemetryEventInfo, IEventNamePropertyMapping } from '../../telemetry';
import { workspace } from 'vscode';
import { workspace, type Disposable } from 'vscode';

/**
* TODO@rebornix
Expand Down Expand Up @@ -45,6 +45,17 @@ export function isTelemetryDisabled(): boolean {
return settings.globalValue === false ? true : false;
}

export function onDidChangeTelemetryEnablement(handler: (enabled: boolean) => void): Disposable {
return workspace.onDidChangeConfiguration((e) => {
if (!e.affectsConfiguration('telemetry')) {
return;
}
const settings = workspace.getConfiguration('telemetry').inspect<boolean>('enableTelemetry')!;
const enabled = settings.globalValue === false ? true : false;
handler(enabled);
});
}

const sharedProperties: Partial<SharedPropertyMapping> = {};
/**
* Set shared properties for all telemetry events.
Expand Down
Loading
Loading