Skip to content

Commit

Permalink
Try to fix ata perf issues on web
Browse files Browse the repository at this point in the history
For microsoft#182791

With this change, we now make a single call to the package manager per root instead of per package. This simplifies the code and should be better for perf

Still seeing a bunch of errors in the console but TS typing is working ok. Needs more exploration for ATA
  • Loading branch information
mjbvz committed Aug 2, 2024
1 parent 3f067de commit 99d5972
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 52 deletions.
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;
}
}

0 comments on commit 99d5972

Please sign in to comment.