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

Try to fix typings perf issues on web #224640

Merged
merged 1 commit into from
Aug 2, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ export function registerAtaSupport(): vscode.Disposable {
requireGlobalConfiguration('typescript', 'tsserver.web.typeAcquisition.enabled'),
], () => {
return vscode.Disposable.from(
// Ata
vscode.workspace.registerFileSystemProvider('vscode-global-typings', new MemFs(), {
isCaseSensitive: true,
isReadonly: false
isReadonly: false,
}),

// Read accesses to node_modules
vscode.workspace.registerFileSystemProvider('vscode-node-modules', new AutoInstallerFs(), {
isCaseSensitive: true,
isReadonly: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,31 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { PackageManager } from '@vscode/ts-package-manager';
import { basename, join } from 'path';
import * as vscode from 'vscode';
import { MemFs } from './memFs';
import { URI } from 'vscode-uri';
import { PackageManager, FileSystem, packagePath } from '@vscode/ts-package-manager';
import { join, basename, dirname } from 'path';
import { Throttler } from '../utils/async';
import { Disposable } from '../utils/dispose';
import { MemFs } from './memFs';

const TEXT_DECODER = new TextDecoder('utf-8');
const TEXT_ENCODER = new TextEncoder();

export class AutoInstallerFs implements vscode.FileSystemProvider {
export class AutoInstallerFs extends Disposable implements vscode.FileSystemProvider {

private readonly memfs = new MemFs();
private readonly fs: FileSystem;
private readonly projectCache = new Map<string, Set<string>>();
private readonly watcher: vscode.FileSystemWatcher;
private readonly _emitter = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
private readonly packageManager: PackageManager;
private readonly _projectCache = new Map</* root */ string, {
readonly throttler: Throttler;
}>();

readonly onDidChangeFile: vscode.Event<vscode.FileChangeEvent[]> = this._emitter.event;
private readonly _emitter = this._register(new vscode.EventEmitter<vscode.FileChangeEvent[]>());
readonly onDidChangeFile = this._emitter.event;

constructor() {
this.watcher = vscode.workspace.createFileSystemWatcher('**/{package.json,package-lock.json,package-lock.kdl}');
const handler = (uri: URI) => {
const root = dirname(uri.path);
if (this.projectCache.delete(root)) {
(async () => {
const pm = new PackageManager(this.fs);
const opts = await this.getInstallOpts(uri, root);
const proj = await pm.resolveProject(root, opts);
proj.pruneExtraneous();
// TODO: should this fire on vscode-node-modules instead?
// NB(kmarchan): This should tell TSServer that there's
// been changes inside node_modules and it needs to
// re-evaluate things.
this._emitter.fire([{
type: vscode.FileChangeType.Changed,
uri: uri.with({ path: join(root, 'node_modules') })
}]);
})();
}
};
this.watcher.onDidChange(handler);
this.watcher.onDidCreate(handler);
this.watcher.onDidDelete(handler);
super();

const memfs = this.memfs;
memfs.onDidChangeFile((e) => {
this._emitter.fire(e.map(ev => ({
Expand All @@ -54,7 +36,8 @@ export class AutoInstallerFs implements vscode.FileSystemProvider {
uri: ev.uri.with({ scheme: 'memfs' })
})));
});
this.fs = {

this.packageManager = new PackageManager({
readDirectory(path: string, _extensions?: readonly string[], _exclude?: readonly string[], _include?: readonly string[], _depth?: number): string[] {
return memfs.readDirectory(URI.file(path)).map(([name, _]) => name);
},
Expand Down Expand Up @@ -87,7 +70,7 @@ export class AutoInstallerFs implements vscode.FileSystemProvider {
return undefined;
}
}
};
});
}

watch(resource: vscode.Uri): vscode.Disposable {
Expand Down Expand Up @@ -151,8 +134,6 @@ export class AutoInstallerFs implements vscode.FileSystemProvider {
}

private async ensurePackageContents(incomingUri: MappedUri): Promise<void> {
// console.log('ensurePackageContents', incomingUri.path);

// If we're not looking for something inside node_modules, bail early.
if (!incomingUri.path.includes('node_modules')) {
throw vscode.FileSystemError.FileNotFound();
Expand All @@ -164,25 +145,26 @@ export class AutoInstallerFs implements vscode.FileSystemProvider {
}

const root = this.getProjectRoot(incomingUri.path);

const pkgPath = packagePath(incomingUri.path);
if (!root || this.projectCache.get(root)?.has(pkgPath)) {
if (!root) {
return;
}
console.log('ensurePackageContents', incomingUri.path, root);

const proj = await (new PackageManager(this.fs)).resolveProject(root, await this.getInstallOpts(incomingUri.original, root));

const restore = proj.restorePackageAt(incomingUri.path);
try {
await restore;
} catch (e) {
console.error(`failed to restore package at ${incomingUri.path}: `, e);
throw e;
let projectEntry = this._projectCache.get(root);
if (!projectEntry) {
projectEntry = { throttler: new Throttler() };
this._projectCache.set(root, projectEntry);
}
if (!this.projectCache.has(root)) {
this.projectCache.set(root, new Set());
}
this.projectCache.get(root)!.add(pkgPath);

projectEntry.throttler.queue(async () => {
const proj = await this.packageManager.resolveProject(root, await this.getInstallOpts(incomingUri.original, root));
try {
await proj.restore();
} catch (e) {
console.error(`failed to restore package at ${incomingUri.path}: `, e);
throw e;
}
});
}

private async getInstallOpts(originalUri: URI, root: string) {
Expand Down
91 changes: 91 additions & 0 deletions extensions/typescript-language-features/src/utils/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,94 @@ export function setImmediate(callback: (...args: any[]) => void, ...args: any[])
return { dispose: () => clearTimeout(handle) };
}
}


/**
* 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 {

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;
}
}
Loading