mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-19 16:18:58 +01:00
Try to fix typings perf issues on web (#224640)
Try to fix ata perf issues on web For #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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 => ({
|
||||
@@ -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);
|
||||
},
|
||||
@@ -87,7 +70,7 @@ export class AutoInstallerFs implements vscode.FileSystemProvider {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
watch(resource: vscode.Uri): vscode.Disposable {
|
||||
@@ -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();
|
||||
@@ -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));
|
||||
let projectEntry = this._projectCache.get(root);
|
||||
if (!projectEntry) {
|
||||
projectEntry = { throttler: new Throttler() };
|
||||
this._projectCache.set(root, projectEntry);
|
||||
}
|
||||
|
||||
const restore = proj.restorePackageAt(incomingUri.path);
|
||||
try {
|
||||
await restore;
|
||||
} catch (e) {
|
||||
console.error(`failed to restore package at ${incomingUri.path}: `, e);
|
||||
throw e;
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user