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

Spawn the extension host directly from the main process because fork() is now fast again #150002

Merged
merged 1 commit into from
May 20, 2022
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
1 change: 0 additions & 1 deletion build/gulpfile.vscode.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ const vscodeResources = [
'out-build/vs/base/browser/ui/codicons/codicon/**',
'out-build/vs/base/parts/sandbox/electron-browser/preload.js',
'out-build/vs/platform/environment/node/userDataPath.js',
'out-build/vs/platform/extensions/node/extensionHostStarterWorkerMain.js',
'out-build/vs/workbench/browser/media/*-theme.css',
'out-build/vs/workbench/contrib/debug/**/*.json',
'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt',
Expand Down
4 changes: 0 additions & 4 deletions src/buildfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@ exports.base = [
},
{
name: 'vs/base/common/worker/simpleWorker',
},
{
name: 'vs/platform/extensions/node/extensionHostStarterWorker',
exclude: ['vs/base/common/worker/simpleWorker']
}
];

Expand Down
4 changes: 2 additions & 2 deletions src/vs/code/electron-main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv';
import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust';
import { ExtensionUrlTrustService } from 'vs/platform/extensionManagement/node/extensionUrlTrustService';
import { IExtensionHostStarter, ipcExtensionHostStarterChannelName } from 'vs/platform/extensions/common/extensionHostStarter';
import { WorkerMainProcessExtensionHostStarter } from 'vs/platform/extensions/electron-main/workerMainProcessExtensionHostStarter';
import { ExtensionHostStarter } from 'vs/platform/extensions/electron-main/extensionHostStarter';
import { IExternalTerminalMainService } from 'vs/platform/externalTerminal/common/externalTerminal';
import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService';
import { LOCAL_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/files/common/diskFileSystemProviderClient';
Expand Down Expand Up @@ -641,7 +641,7 @@ export class CodeApplication extends Disposable {
services.set(IExtensionUrlTrustService, new SyncDescriptor(ExtensionUrlTrustService));

// Extension Host Starter
services.set(IExtensionHostStarter, new SyncDescriptor(WorkerMainProcessExtensionHostStarter));
services.set(IExtensionHostStarter, new SyncDescriptor(ExtensionHostStarter));

// Storage
services.set(IStorageMainService, new SyncDescriptor(StorageMainService));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,134 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { canceled, SerializedError, transformErrorForSerialization } from 'vs/base/common/errors';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter';
import { Emitter, Event } from 'vs/base/common/event';
import { ILogService } from 'vs/platform/log/common/log';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { StopWatch } from 'vs/base/common/stopwatch';
import { ChildProcess, fork } from 'child_process';
import { StringDecoder } from 'string_decoder';
import { Promises, timeout } from 'vs/base/common/async';
import { SerializedError, transformErrorForSerialization } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { FileAccess } from 'vs/base/common/network';
import { mixin } from 'vs/base/common/objects';
import * as platform from 'vs/base/common/platform';
import { cwd } from 'vs/base/common/process';
import { StopWatch } from 'vs/base/common/stopwatch';
import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter';

export interface IExtensionHostStarterWorkerHost {
logInfo(message: string): Promise<void>;
export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter {
_serviceBrand: undefined;

private static _lastId: number = 0;

protected readonly _extHosts: Map<string, ExtensionHostProcess>;
private _shutdown = false;

constructor(
@ILogService private readonly _logService: ILogService,
@ILifecycleMainService lifecycleMainService: ILifecycleMainService
) {
this._extHosts = new Map<string, ExtensionHostProcess>();

// On shutdown: gracefully await extension host shutdowns
lifecycleMainService.onWillShutdown((e) => {
this._shutdown = true;
e.join(this._waitForAllExit(6000));
});
}

dispose(): void {
// Intentionally not killing the extension host processes
}

private _getExtHost(id: string): ExtensionHostProcess {
const extHostProcess = this._extHosts.get(id);
if (!extHostProcess) {
throw new Error(`Unknown extension host!`);
}
return extHostProcess;
}

onDynamicStdout(id: string): Event<string> {
return this._getExtHost(id).onStdout;
}

onDynamicStderr(id: string): Event<string> {
return this._getExtHost(id).onStderr;
}

onDynamicMessage(id: string): Event<any> {
return this._getExtHost(id).onMessage;
}

onDynamicError(id: string): Event<{ error: SerializedError }> {
return this._getExtHost(id).onError;
}

onDynamicExit(id: string): Event<{ code: number; signal: string }> {
return this._getExtHost(id).onExit;
}

async createExtensionHost(): Promise<{ id: string }> {
if (this._shutdown) {
throw canceled();
}
const id = String(++ExtensionHostStarter._lastId);
const extHost = new ExtensionHostProcess(id, this._logService);
this._extHosts.set(id, extHost);
extHost.onExit(({ pid, code, signal }) => {
this._logService.info(`Extension host with pid ${pid} exited with code: ${code}, signal: ${signal}.`);
setTimeout(() => {
extHost.dispose();
this._extHosts.delete(id);
});
});
return { id };
}

async start(id: string, opts: IExtensionHostProcessOptions): Promise<{ pid: number }> {
if (this._shutdown) {
throw canceled();
}
return this._getExtHost(id).start(opts);
}

async enableInspectPort(id: string): Promise<boolean> {
if (this._shutdown) {
throw canceled();
}
const extHostProcess = this._extHosts.get(id);
if (!extHostProcess) {
return false;
}
return extHostProcess.enableInspectPort();
}

async kill(id: string): Promise<void> {
if (this._shutdown) {
throw canceled();
}
const extHostProcess = this._extHosts.get(id);
if (!extHostProcess) {
// already gone!
return;
}
extHostProcess.kill();
}

async _killAllNow(): Promise<void> {
for (const [, extHost] of this._extHosts) {
extHost.kill();
}
}

async _waitForAllExit(maxWaitTimeMs: number): Promise<void> {
const exitPromises: Promise<void>[] = [];
for (const [, extHost] of this._extHosts) {
exitPromises.push(extHost.waitForExit(maxWaitTimeMs));
}
return Promises.settled(exitPromises).then(() => { });
}
}

class ExtensionHostProcess extends Disposable {
Expand All @@ -42,14 +155,14 @@ class ExtensionHostProcess extends Disposable {

constructor(
public readonly id: string,
private readonly _host: IExtensionHostStarterWorkerHost
@ILogService private readonly _logService: ILogService,
) {
super();
}

start(opts: IExtensionHostProcessOptions): { pid: number } {
if (platform.isCI) {
this._host.logInfo(`Calling fork to start extension host...`);
this._logService.info(`Calling fork to start extension host...`);
}
const sw = StopWatch.create(false);
this._process = fork(
Expand All @@ -60,7 +173,7 @@ class ExtensionHostProcess extends Disposable {
const forkTime = sw.elapsed();
const pid = this._process.pid!;

this._host.logInfo(`Starting extension host with pid ${pid} (fork() took ${forkTime} ms).`);
this._logService.info(`Starting extension host with pid ${pid} (fork() took ${forkTime} ms).`);

const stdoutDecoder = new StringDecoder('utf-8');
this._process.stdout?.on('data', (chunk) => {
Expand Down Expand Up @@ -95,7 +208,7 @@ class ExtensionHostProcess extends Disposable {
return false;
}

this._host.logInfo(`Enabling inspect port on extension host with pid ${this._process.pid}.`);
this._logService.info(`Enabling inspect port on extension host with pid ${this._process.pid}.`);

interface ProcessExt {
_debugProcess?(n: number): any;
Expand All @@ -119,7 +232,7 @@ class ExtensionHostProcess extends Disposable {
if (!this._process) {
return;
}
this._host.logInfo(`Killing extension host with pid ${this._process.pid}.`);
this._logService.info(`Killing extension host with pid ${this._process.pid}.`);
this._process.kill();
}

Expand All @@ -128,116 +241,13 @@ class ExtensionHostProcess extends Disposable {
return;
}
const pid = this._process.pid;
this._host.logInfo(`Waiting for extension host with pid ${pid} to exit.`);
this._logService.info(`Waiting for extension host with pid ${pid} to exit.`);
await Promise.race([Event.toPromise(this.onExit), timeout(maxWaitTimeMs)]);

if (!this._hasExited) {
// looks like we timed out
this._host.logInfo(`Extension host with pid ${pid} did not exit within ${maxWaitTimeMs}ms.`);
this._logService.info(`Extension host with pid ${pid} did not exit within ${maxWaitTimeMs}ms.`);
this._process.kill();
}
}
}

export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter {
_serviceBrand: undefined;

private static _lastId: number = 0;

protected readonly _extHosts: Map<string, ExtensionHostProcess>;

constructor(
private readonly _host: IExtensionHostStarterWorkerHost
) {
this._extHosts = new Map<string, ExtensionHostProcess>();
}

dispose(): void {
// Intentionally not killing the extension host processes
}

private _getExtHost(id: string): ExtensionHostProcess {
const extHostProcess = this._extHosts.get(id);
if (!extHostProcess) {
throw new Error(`Unknown extension host!`);
}
return extHostProcess;
}

onDynamicStdout(id: string): Event<string> {
return this._getExtHost(id).onStdout;
}

onDynamicStderr(id: string): Event<string> {
return this._getExtHost(id).onStderr;
}

onDynamicMessage(id: string): Event<any> {
return this._getExtHost(id).onMessage;
}

onDynamicError(id: string): Event<{ error: SerializedError }> {
return this._getExtHost(id).onError;
}

onDynamicExit(id: string): Event<{ code: number; signal: string }> {
return this._getExtHost(id).onExit;
}

async createExtensionHost(): Promise<{ id: string }> {
const id = String(++ExtensionHostStarter._lastId);
const extHost = new ExtensionHostProcess(id, this._host);
this._extHosts.set(id, extHost);
extHost.onExit(({ pid, code, signal }) => {
this._host.logInfo(`Extension host with pid ${pid} exited with code: ${code}, signal: ${signal}.`);
setTimeout(() => {
extHost.dispose();
this._extHosts.delete(id);
});
});
return { id };
}

async start(id: string, opts: IExtensionHostProcessOptions): Promise<{ pid: number }> {
return this._getExtHost(id).start(opts);
}

async enableInspectPort(id: string): Promise<boolean> {
const extHostProcess = this._extHosts.get(id);
if (!extHostProcess) {
return false;
}
return extHostProcess.enableInspectPort();
}

async kill(id: string): Promise<void> {
const extHostProcess = this._extHosts.get(id);
if (!extHostProcess) {
// already gone!
return;
}
extHostProcess.kill();
}

async killAllNow(): Promise<void> {
for (const [, extHost] of this._extHosts) {
extHost.kill();
}
}

async waitForAllExit(maxWaitTimeMs: number): Promise<void> {
const exitPromises: Promise<void>[] = [];
for (const [, extHost] of this._extHosts) {
exitPromises.push(extHost.waitForExit(maxWaitTimeMs));
}
return Promises.settled(exitPromises).then(() => { });
}
}

/**
* The `create` function needs to be there by convention because
* we are loaded via the `vs/base/common/worker/simpleWorker` utility.
*/
export function create(host: IExtensionHostStarterWorkerHost) {
return new ExtensionHostStarter(host);
}
Loading